//
-// ### Socials
+// ## Socials
//
// - Check out our [community projects](https://github.com/gnolang/awesome-gno)
-// - ![Discord](static/img/ico-discord.svg) [Discord](https://discord.gg/S8nKUqwkPn)
-// - ![Twitter](static/img/ico-twitter.svg) [Twitter](https://twitter.com/_gnoland)
-// - ![Youtube](static/img/ico-youtube.svg) [Youtube](https://www.youtube.com/@_gnoland)
-// - ![Telegram](static/img/ico-telegram.svg) [Telegram](https://t.me/gnoland)
+// - [Discord](https://discord.gg/S8nKUqwkPn)
+// - [Twitter](https://twitter.com/_gnoland)
+// - [Youtube](https://www.youtube.com/@_gnoland)
+// - [Telegram](https://t.me/gnoland)
//
//
//
-// ### Quote of the ~Day~ Block#123
+// ## Quote of the ~Day~ Block#123
//
// > Now, you Gno.
//
diff --git a/examples/gno.land/r/gnoland/home/overide_filetest.gno b/examples/gno.land/r/gnoland/home/overide_filetest.gno
index 4f21b90a3c2..be7e33501d6 100644
--- a/examples/gno.land/r/gnoland/home/overide_filetest.gno
+++ b/examples/gno.land/r/gnoland/home/overide_filetest.gno
@@ -8,7 +8,7 @@ import (
)
func main() {
- std.TestSetOrigCaller("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq")
+ std.TestSetOrigCaller("g1manfred47kzduec920z88wfr64ylksmdcedlf5")
home.AdminSetOverride("Hello World!")
println(home.Render(""))
home.AdminTransferOwnership(testutils.TestAddress("newAdmin"))
diff --git a/examples/gno.land/r/gnoland/monit/gno.mod b/examples/gno.land/r/gnoland/monit/gno.mod
index e67fdaa7d71..6086a3fa21f 100644
--- a/examples/gno.land/r/gnoland/monit/gno.mod
+++ b/examples/gno.land/r/gnoland/monit/gno.mod
@@ -1,8 +1 @@
module gno.land/r/gnoland/monit
-
-require (
- gno.land/p/demo/ownable v0.0.0-latest
- gno.land/p/demo/uassert v0.0.0-latest
- gno.land/p/demo/ufmt v0.0.0-latest
- gno.land/p/demo/watchdog v0.0.0-latest
-)
diff --git a/examples/gno.land/r/gnoland/monit/monit.gno b/examples/gno.land/r/gnoland/monit/monit.gno
index 8747ea582b3..be94fbdd2bb 100644
--- a/examples/gno.land/r/gnoland/monit/monit.gno
+++ b/examples/gno.land/r/gnoland/monit/monit.gno
@@ -20,7 +20,7 @@ var (
lastUpdate time.Time
lastCaller std.Address
wd = watchdog.Watchdog{Duration: 5 * time.Minute}
- owner = ownable.New() // TODO: replace with -> ownable.NewWithAddress...
+ Ownable = ownable.New() // TODO: replace with -> ownable.NewWithAddress...
watchdogDuration = 5 * time.Minute
)
@@ -37,9 +37,8 @@ func Incr() int {
// Reset resets the realm state.
// This function can only be called by the admin.
func Reset() {
- if owner.CallerIsOwner() != nil { // TODO: replace with owner.AssertCallerIsOwner
- panic("unauthorized")
- }
+ Ownable.AssertCallerIsOwner()
+
counter = 0
lastCaller = std.PrevRealm().Addr()
lastUpdate = time.Now()
@@ -53,7 +52,3 @@ func Render(_ string) string {
counter, lastUpdate, lastCaller, status,
)
}
-
-// TransferOwnership transfers ownership to a new owner. This is a proxy to
-// ownable.Ownable.TransferOwnership.
-func TransferOwnership(newOwner std.Address) { owner.TransferOwnership(newOwner) }
diff --git a/examples/gno.land/r/gnoland/pages/admin.gno b/examples/gno.land/r/gnoland/pages/admin.gno
index ab447e8f604..71050f4ef57 100644
--- a/examples/gno.land/r/gnoland/pages/admin.gno
+++ b/examples/gno.land/r/gnoland/pages/admin.gno
@@ -15,7 +15,7 @@ var (
func init() {
// adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis.
- adminAddr = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq"
+ adminAddr = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul
}
func AdminSetAdminAddr(addr std.Address) {
diff --git a/examples/gno.land/r/gnoland/pages/gno.mod b/examples/gno.land/r/gnoland/pages/gno.mod
index 31e9ad2c85b..e041fd948bc 100644
--- a/examples/gno.land/r/gnoland/pages/gno.mod
+++ b/examples/gno.land/r/gnoland/pages/gno.mod
@@ -1,6 +1 @@
module gno.land/r/gnoland/pages
-
-require (
- gno.land/p/demo/avl v0.0.0-latest
- gno.land/p/demo/blog v0.0.0-latest
-)
diff --git a/examples/gno.land/r/gnoland/pages/page_about.gno b/examples/gno.land/r/gnoland/pages/page_about.gno
index 6b1f5a6c556..99a879b4ba3 100644
--- a/examples/gno.land/r/gnoland/pages/page_about.gno
+++ b/examples/gno.land/r/gnoland/pages/page_about.gno
@@ -2,28 +2,28 @@ package gnopages
func init() {
path := "about"
- title := "Gno.land Is A Platform To Write Smart Contracts In Gno"
+ title := "gno.land Is A Platform To Write Smart Contracts In Gno"
// XXX: description := "On gno.land, developers write smart contracts and other blockchain apps using Gno without learning a language that’s exclusive to a single ecosystem."
body := `
-Gno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go
+gno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go
programming language. On gno.land, smart contracts can be uploaded on-chain only by publishing their full source code,
-making it trivial to verify the contract or fork it into an improved version. With a system to publish reusable code
-libraries on-chain, gno.land serves as the “GitHub” of the ecosystem, with realms built using fully transparent,
+making it trivial to verify the contract or fork it into an improved version. With a system to publish reusable code
+libraries on-chain, gno.land serves as the “GitHub” of the ecosystem, with realms built using fully transparent,
auditable code that anyone can inspect and reuse.
-Gno.land addresses many pressing issues in the blockchain space, starting with the ease of use and intuitiveness of
-smart contract platforms. Developers can write smart contracts without having to learn a new language that’s exclusive
+gno.land addresses many pressing issues in the blockchain space, starting with the ease of use and intuitiveness of
+smart contract platforms. Developers can write smart contracts without having to learn a new language that’s exclusive
to a single ecosystem or limited by design. Go developers can easily port their existing web apps to gno.land or build
new ones from scratch, making web3 vastly more accessible.
-Secured by Proof of Contribution (PoC), a DAO-managed Proof-of-Authority consensus mechanism, gno.land prioritizes
-fairness and merit, rewarding the people most active on the platform. PoC restructures the financial incentives that
-often corrupt blockchain projects, opting instead to reward contributors for their work based on expertise, commitment, and
-alignment.
+Secured by Proof of Contribution (PoC), a DAO-managed Proof-of-Authority consensus mechanism, gno.land prioritizes
+fairness and merit, rewarding the people most active on the platform. PoC restructures the financial incentives that
+often corrupt blockchain projects, opting instead to reward contributors for their work based on expertise, commitment, and
+alignment.
One of our inspirations for gno.land is the gospels, which built a system of moral code that lasted thousands of years.
-By observing a minimal production implementation, gno.land’s design will endure over time and serve as a reference for
-future generations with censorship-resistant tools that improve their understanding of the world.
+By observing a minimal production implementation, gno.land’s design will endure over time and serve as a reference for
+future generations with censorship-resistant tools that improve their understanding of the world.
`
_ = b.NewPost("", path, title, body, "2022-05-20T13:17:22Z", nil, nil)
}
diff --git a/examples/gno.land/r/gnoland/pages/page_contribute.gno b/examples/gno.land/r/gnoland/pages/page_contribute.gno
new file mode 100644
index 00000000000..0855dc327cd
--- /dev/null
+++ b/examples/gno.land/r/gnoland/pages/page_contribute.gno
@@ -0,0 +1,106 @@
+package gnopages
+
+func init() {
+ path := "contribute"
+ title := "Contributor Ecosystem: Call for Contributions"
+ body := `
+
+gno.land puts at the center of its identity the contributors that help to create and shape the project into what it is; incentivizing those who contribute the most and help advance its vision. Eventually, contributions will be incentivized directly on-chain; in the meantime, this page serves to illustrate our current off-chain initiatives.
+
+gno.land is still in full-steam development. For now, we're looking for the earliest of adopters; curious to explore a new way to build smart contracts and eager to make an impact. Joining gno.land's development now means you can help to shape the base of its development ecosystem, which will pave the way for the next generation of blockchain programming.
+
+As an open-source project, we welcome all contributions. On this page you can find some pointers on where to get started; as well as some incentives for the most valuable and important contributions.
+
+## Where to get started
+
+If you are interested in contributing to gno.land, you can jump on in on our [GitHub monorepo](https://github.com/gnolang/gno/blob/master/CONTRIBUTING.md) - where most development happens.
+
+A good place where to start are the issues tagged ["good first issue"](https://github.com/gnolang/gno/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). They should allow you to make some impact on the Gno repository while you're still exploring the details of how everything works.
+
+## Gno Bounties
+
+Additionally, you can look out to help on specific issues labeled as bounties. All contributions will then concur to form your profile for Game of Realms.
+
+The Gno bounty program is a good way to find interesting challenges in Gno, and get rewarded for helping us advance the project. We will maintain open and rewardable bounties in the gnolang/gno repository, and you can search all available bounties by using the ["bounty" label](https://github.com/gnolang/gno/labels/bounty).
+
+Recommendations on participating in the gno.land Bounty Program:
+
+- Identify the bounty you want to work on, and join in the discussion on the issue for anything that is unclear; or where you want to more clearly define the work to be done. At this stage, you can also start working on an initial implementation in your local enviornment.
+- Once you have spent time on the code related to the bounty, we recommend submitting a 'draft' PR as soon as possible.
+ - The draft PR doesn't indicate that the bounty has been assigned to you, others are free to work on other draft PRs for the bounty.
+ - Make sure to reference the bounty issue on the PR description you're writing.
+ - After submitting the 'draft' PR, continue working until you are ready to mark the PR as "ready for review".
+ - The core team will review the bounty PR submission after the work on the bounty has been completed, and determine if it qualifies for the bounty reward.
+- Ask for clarification early if an element on the requirements or implementation design is unclear.
+ - Aside from publishing the PR early, keeping regular updates with the core team on the bounty issue is key to being on the right track.
+ - As part of the requirements, you must adhere to the [contributing guidelines](https://github.com/gnolang/gno/blob/master/CONTRIBUTING.md); additionally, it is expected that any newly added code or functionality is properly documented, tested and covered, at least in 80% of added code.
+ - You're welcome to propose additional features and work on an issue should you envision a plausible expansion or change in scope. The core team may assign a bounty to the additional work, or change the bounty with respect to the changed scope.
+
+You may make your submission at any time; however we invite you to publish your draft PR very early in the development process. This will make your work public, so you can easily get help by the core team and other community members. Additionally, your work can be continued by other people should you get stuck or no longer be willing to work on the bounty. Likewise, you can continue the abandoned or stuck work that someone else worked on.
+
+Don't fear your work being "stolen": if a submission is the result of multiple people's efforts, we will look to split the bounty in a way that is fair and recognises each participant in creating the final outcome. Here are some examples of how that can happen:
+
+- If Alice does most of the work and abandons it; then Bob comes around and finishes the job, then Bob's PR will be merged. But the core team will propose a split like 70% for Alice and 30% for Bob (depending, of course, on the relative effort undertaken by both).
+- If Alice makes a PR that does only 50% of the work outlined in the requirements for the original issue, she will get 50%. Someone can still come up and finish the job; and claim the remaining part.
+ - If you, for instance, cannot complete the entirety of the task or, as a non-developer, can only contribute a part of the specification/implementation, you may still be awarded a bounty for your input in the contribution.
+- If Alice makes a PR that aside from implementing what's required, also undertakes creating useful tools among the way, she may qualify for an "outstanding contribution"; and may be awarded up to 25% more of the original bounty's value. Or she may also ask if the team would be willing to offer a different bounty for the implementation of the tools.
+
+Participants in the gno.land Bounty Program must meet the legal Terms and Conditions referenced [here](https://docs.google.com/document/d/e/2PACX-1vSUF-JwIXGscrNsc5QBD7Pa6i83mXUGogAEIf1wkeb_w42UgL3Lj6jFKMlNTdwEMUnhsLkjRlhe25K4/pub).
+
+### Bounty sizes
+
+Each bounty is associated with a size, to which corresponds the maximum compensation for the work involved on the bounty. A bounty size may under rare occasion be revisited to a bigger or smaller size; hence why it's important to talk about your proposed solution with the core team ahead of time.
+
+In some cases, the work associated with a bounty may be outstanding. When that happens, the core team can decide to award up to 25% of the bounty's value to the recipient.
+
+The value of the bounty, aside from the material completion of the task, considers the involved time in managing the created pull request and iterating on feedback.
+
+
+t-shirt size | expected compensation
+-------------|-----------------------
+[XS] | $ 500
+[S] | $ 1000
+[M] | $ 2000
+[L] | $ 4000
+[XL] | $ 8000
+_[XXL]_ \* | $ 16000
+_[3XL]_ \* | $ 32000
+
+[XS]: https://github.com/gnolang/gno/labels/bounty%2FXS
+[S]: https://github.com/gnolang/gno/labels/bounty%2FS
+[M]: https://github.com/gnolang/gno/labels/bounty%2FM
+[L]: https://github.com/gnolang/gno/labels/bounty%2FL
+[XL]: https://github.com/gnolang/gno/labels/bounty%2FXL
+[XXL]: https://github.com/gnolang/gno/labels/bounty%2FXXL
+[3XL]: https://github.com/gnolang/gno/labels/bounty%2F3XL
+
+\*: XXL and 3XL bounties are exceptional. Almost no issues will have these sizes; most will be broken down into smaller bounties.
+
+## gno.land Grants
+
+The gno.land grants program is to encourage and support the growth of the gno.land contributor community, and build out the usability of the platform and smart contract library. The program provides financial resources to contributors to explore the Gno tech stack, and build dApps, tooling, infrastructure, products, and smart contract libraries in gno.land.
+
+For more details on gno.land grants, suggested topics, and how to apply, visit our grants [repository](https://github.com/gnolang/grants).
+
+## Join Game of Realms
+
+Game of Realms is the overarching contributor network of gnomes, currently running off-chain, and will eventually transition on-chain. At this stage, a Game of Realms contribution is comprised of high-impact contributions identified as ['notable contributions'](https://github.com/gnolang/game-of-realms/tree/main/contributors).
+
+These contributions are not linked to immediate financial rewards, but are notable in nature, in the sense they are a challenge, make a significant addition to the project, and require persistence, with minimal feedback loops from the core team.
+
+The selection of a notable contribution or the sum of contributions that equal 'notable' is based on the impact it has on the development of the project. For now, it is focused on code contributions, and will evolve over time. The Gno development teams will initially qualify and evaluate notable contributions, and vote off-chain on adding them to the 'notable contributions' folder on GitHub.
+
+You can always contribute to the project, and all contributions will be noticed. Contributing now is a way to build your personal contributor profile in gno.land early on in the ecosystem, and signal your commitment to the project, the community, and its future.
+
+There are a variety of ways to make your contributions count:
+
+- Core code contributions
+- Realm and pure package development
+- Validator tooling
+- Developer tooling
+- Tutorials and documentation
+
+To start, we recommend you create a PR in the Game of Realms [repository](https://github.com/gnolang/game-of-realms) to create your profile page for all your contributions.`
+
+ _ = b.NewPost("", path, title, body, "2024-09-05T00:00:00Z", nil, nil)
+}
diff --git a/examples/gno.land/r/gnoland/pages/page_ecosystem.gno b/examples/gno.land/r/gnoland/pages/page_ecosystem.gno
index e1a540c98a5..514ea7b2a98 100644
--- a/examples/gno.land/r/gnoland/pages/page_ecosystem.gno
+++ b/examples/gno.land/r/gnoland/pages/page_ecosystem.gno
@@ -3,42 +3,56 @@ package gnopages
func init() {
var (
path = "ecosystem"
- title = "Discover Gno.land Ecosystem Projects & Initiatives"
+ title = "Discover gno.land Ecosystem Projects & Initiatives"
// XXX: description = "Dive further into the gno.land ecosystem and discover the core infrastructure, projects, smart contracts, and tooling we’re building."
body = `
### [Gno Playground](https://play.gno.land)
-Gno Playground is a simple web interface that lets you write, test, and experiment with your Gno code to improve your
+Gno Playground is a simple web interface that lets you write, test, and experiment with your Gno code to improve your
understanding of the Gno language. You can share your code, run unit tests, deploy your realms and packages, and execute
-functions in your code using the repo.
+functions in your code using the repo.
Visit the playground at [play.gno.land](https://play.gno.land)!
+### [Gno Studio Connect](https://gno.studio/connect)
+
+Gno Studio Connect provides seamless access to realms, making it simple to explore, interact, and engage
+with gno.land’s smart contracts through function calls. Connect focuses on function calls, enabling users to interact
+with any realm’s exposed function(s) on gno.land.
+
+See your realm interactions in [Gno Studio Connect](https://gno.studio/connect)
+
### [Gnoscan](https://gnoscan.io)
Developed by the Onbloc team, Gnoscan is gno.land’s blockchain explorer. Anyone can use Gnoscan to easily find
-information that resides on the gno.land blockchain, such as wallet addresses, TX hashes, blocks, and contracts.
+information that resides on the gno.land blockchain, such as wallet addresses, TX hashes, blocks, and contracts.
Gnoscan makes our on-chain data easy to read and intuitive to discover.
Explore the gno.land blockchain at [gnoscan.io](https://gnoscan.io)!
### Adena
-Adena is a user-friendly non-custodial wallet for gno.land. Open-source and developed by Onbloc, Adena allows gnomes to
+Adena is a user-friendly non-custodial wallet for gno.land. Open-source and developed by Onbloc, Adena allows gnomes to
interact easily with the chain. With an emphasis on UX, Adena is built to handle millions of realms and tokens with a
-high-quality interface, support for NFTs and custom tokens, and seamless integration.
+high-quality interface, support for NFTs and custom tokens, and seamless integration. Install Adena via the [official website](https://www.adena.app/)
### Gnoswap
-Gnoswap is currently under development and led by the Onbloc team. Gnoswap will be the first DEX on gno.land and is an
+Gnoswap is currently under development and led by the Onbloc team. Gnoswap will be the first DEX on gno.land and is an
automated market maker (AMM) protocol written in Gno that allows for permissionless token exchanges on the platform.
### Flippando
Flippando is a simple on-chain memory game, ported from Solidity to Gno, which starts with an empty matrix to flip tiles
-on to see what’s underneath. If the tiles match, they remain uncovered; if not, they are briefly shown, and the player
+on to see what’s underneath. If the tiles match, they remain uncovered; if not, they are briefly shown, and the player
must memorize their colors until the entire matrix is uncovered. The end result can be minted as an NFT, which can later
-be assembled into bigger, more complex NFTs, creating a digital “painting” with the uncovered tiles.
+be assembled into bigger, more complex NFTs, creating a digital “painting” with the uncovered tiles. Play the game at [Flippando](https://gno.flippando.xyz/flip)
+
+### Gno Native Kit
+
+[Gno Native Kit](https://github.com/gnolang/gnonative) is a framework that allows developers to build and port gno.land (d)apps written in the (d)app's native language.
+
+
`
)
_ = b.NewPost("", path, title, body, "2022-05-20T13:17:23Z", nil, nil)
diff --git a/examples/gno.land/r/gnoland/pages/page_gnolang.gno b/examples/gno.land/r/gnoland/pages/page_gnolang.gno
index 13fc4072b1a..ac7bd9025b0 100644
--- a/examples/gno.land/r/gnoland/pages/page_gnolang.gno
+++ b/examples/gno.land/r/gnoland/pages/page_gnolang.gno
@@ -3,7 +3,7 @@ package gnopages
func init() {
var (
path = "gnolang"
- title = "About the Gno, the Language for Gno.land"
+ title = "About the Gno, the Language for gno.land"
// TODO fix broken images
body = `
diff --git a/examples/gno.land/r/gnoland/pages/page_gor.gno b/examples/gno.land/r/gnoland/pages/page_gor.gno
deleted file mode 100644
index d46e9cb0ccc..00000000000
--- a/examples/gno.land/r/gnoland/pages/page_gor.gno
+++ /dev/null
@@ -1,221 +0,0 @@
-package gnopages
-
-func init() {
- path := "gor"
- title := "Game of Realms - A Contest For The Best Contributors"
- // XXX: description := "Game of Realms is the first high-stakes competition held in two phases to find the best contributors to the gno.land platform with a 133,700 ATOM prize pool."
- body := `
-
-
-
-### Game of Realms
-
-The first high-stakes contest will see participants compete for tiered membership to co-own the gno.land blockchain. A series of complex technical and non-technical tasks will challenge contributors to create innovative patterns that push the chain to new limits. Start building the foundation for tomorrow through key smart contracts and other contributions that change our understanding of the world.
-
-
-
-The competition is currently in phase one – for advanced developers only.
-
-Once the necessary tools to start phase two are ready, we’ll open up the competition to newer devs and non-technical contributors.
-
-If you want to stack ATOM rewards and play a key role in the success of gno.land and web3, read more about Game of Realms or open a [PR](https://github.com/gnolang/gno/) today.
-
-
-
-
-
-
-
-## Phase I. (ongoing)
-
-- Evaluation DAO (30%)
-
-- Tutorials (80%)
-
-- Governance Module (25%)
-
-
-
-
-## Phase II. (Locked)
-
-
-
-
-
-
-
-
-
-## Evaluation DAO
-
-This complex challenge seeks your skills in DAO development and implementation and is one of the most important challenges of phase one. The Evaluation DAO will ensure that contributions in Game of Realms and the gno.land platform are fairly rewarded.
-
-
-
-
☑ Clarifying this issue — [100% completed]
-
-
-
-
-
☐ Retrospectives & investigations — [20% In progress]
-
-
-
-Game of Realms participants and core contributors are still in discussions, proposing additional ideas, and seeing how the proposal for the Evaluation DAO evolves over time.
-
-
-
-
☐ Human specs — definitions, rules, examples — [20% In progress]
-
-
-
-See [GitHub issue 519](https://github.com/gnolang/gno/issues/519) for the most up-to-date discussion so far on how voting should work for the DAO, what the responsibilities are, how to join, etc.
-
-
-
-
☐ Technical specs and interfaces — [0% Stand-by]
-
-
-
-
-
☐ Implementation — [0% Stand-by]
-
-
-
-
-
☐ Documentation — [0% Stand-by]
-
-
-
-
-
☐ Bootstrapping plan — [0% Stand-by]
-
-
-
-
-
-
-
-
-
-## Tutorials
-
-To progress to phase two of the competition, we need high-quality tutorials, guides, and documentation from phase one participants. Help to create materials that will onboard more contributors to gno.land.
-
-
-
-
☑ Clarifying this issue — [100% completed]
-
-
-
-
-
☑ Retrospectives & investigations — [100% completed]
-
-
-
-How to create, present, and house the tutorials has been established with productive discussions between core contributors and external participants.
-
-
-
-
☑ Human specs — definitions, rules, examples — [100% completed]
-
-
-
-We followed a collaborative approach to defining the scope of the work and creating a series of tutorials and videos, Gno by Example, to explain core concepts and show newcomers how to write in the Gno programming language. Gno docs and tutorials will be community-run so that anyone can contribute. Onbloc’s [developer portal](https://docs.onbloc.xyz/) is an excellent onramp to gno.land currently. We will soon be releasing a documentation instance to house all tutorials.
-
-
-
-
☑ Technical specs and interfaces — [100% completed]
-
-
-
-
-
☐ Implementation — [80% In progress]
-
-
-
-
-
☐ Bootstrapping plan — [0% Stand-by]
-
-
-
-
-
-
-
-
-
-## Governance Module
-
-Can you define and implement a governance contract suite that rivals existing ones, such as the Cosmos Hub? Show us how! We’re looking for the fairest and most efficient governance solution possible.
-
-
-
-
☑ Clarifying this issue — [100% completed]
-
-
-
-
-
☐ Retrospectives & investigations — [60% In progress]
-
-
-
-Game of Realms participants and core contributors have made significant progress teaming up to complete this challenge but discussions and additional ideas are still ongoing.
-
-
-
-
☐ Human specs — definitions, rules, examples — [20% In progress]
-
-
-
-
-
☐ Technical specs and interfaces — [0% Stand-by]
-
-
-
-
-
☐ Implementation — [0% Stand-by]
-
-
-
-
-
☐ Documentation — [0% Stand-by]
-
-
-
-
☐ Bootstrapping plan — [0% Stand-by]
-
-
-
-
-
-
-
-
-
-## Register Now
-
-
-
-
-`
- _ = b.NewPost("", path, title, body, "2022-05-20T13:17:26Z", nil, nil)
-}
diff --git a/examples/gno.land/r/gnoland/pages/page_testnets.gno b/examples/gno.land/r/gnoland/pages/page_testnets.gno
index 05f29a8e0f4..0811cd68e6d 100644
--- a/examples/gno.land/r/gnoland/pages/page_testnets.gno
+++ b/examples/gno.land/r/gnoland/pages/page_testnets.gno
@@ -2,14 +2,11 @@ package gnopages
func init() {
path := "testnets"
- title := "Gno.land Testnet List"
+ title := "gno.land Testnet List"
body := `
-- [Portal Loop](https://docs.gno.land/concepts/portal-loop) - a rolling testnet
+- [Portal Loop](https://docs.gno.land/concepts/portal-loop) - a rolling testnet
- [staging.gno.land](https://staging.gno.land) - wiped every commit to monorepo master
-- test4.gno.land (upcoming)
-- _[test3.gno.land](https://test3.gno.land) (latest)_
-- _[test2.gno.land](https://test2.gno.land) (archive)_
-- _[test1.gno.land](https://test1.gno.land) (archive)_
+- _[test4.gno.land](https://test4.gno.land) (latest)_
For a list of RPC endpoints, see the [reference documentation](https://docs.gno.land/reference/rpc-endpoints).
diff --git a/examples/gno.land/r/gnoland/pages/page_tokenomics.gno b/examples/gno.land/r/gnoland/pages/page_tokenomics.gno
index f51364c36e6..3070e58cc6f 100644
--- a/examples/gno.land/r/gnoland/pages/page_tokenomics.gno
+++ b/examples/gno.land/r/gnoland/pages/page_tokenomics.gno
@@ -3,7 +3,7 @@ package gnopages
func init() {
var (
path = "tokenomics"
- title = "Gno.land Tokenomics"
+ title = "gno.land Tokenomics"
// XXX: description = """
body = `Lorem Ipsum`
)
diff --git a/examples/gno.land/r/gnoland/pages/pages_test.gno b/examples/gno.land/r/gnoland/pages/pages_test.gno
index c7972686bb3..16984a1c7ff 100644
--- a/examples/gno.land/r/gnoland/pages/pages_test.gno
+++ b/examples/gno.land/r/gnoland/pages/pages_test.gno
@@ -11,7 +11,7 @@ func TestHome(t *testing.T) {
expectedSubtrings := []string{
"/r/gnoland/pages:p/tokenomics",
"/r/gnoland/pages:p/start",
- "/r/gnoland/pages:p/gor",
+ "/r/gnoland/pages:p/contribute",
"/r/gnoland/pages:p/about",
"/r/gnoland/pages:p/gnolang",
}
@@ -30,8 +30,8 @@ func TestAbout(t *testing.T) {
printedOnce := false
got := Render("p/about")
expectedSubtrings := []string{
- "Gno.land Is A Platform To Write Smart Contracts In Gno",
- "Gno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go\nprogramming language.",
+ "gno.land Is A Platform To Write Smart Contracts In Gno",
+ "gno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go\nprogramming language.",
}
for _, substring := range expectedSubtrings {
if !strings.Contains(got, substring) {
diff --git a/examples/gno.land/r/gnoland/valopers/gno.mod b/examples/gno.land/r/gnoland/valopers/gno.mod
deleted file mode 100644
index 2d24fb27952..00000000000
--- a/examples/gno.land/r/gnoland/valopers/gno.mod
+++ /dev/null
@@ -1,11 +0,0 @@
-module gno.land/r/gnoland/valopers
-
-require (
- gno.land/p/demo/avl v0.0.0-latest
- gno.land/p/demo/testutils v0.0.0-latest
- gno.land/p/demo/uassert v0.0.0-latest
- gno.land/p/demo/ufmt v0.0.0-latest
- gno.land/p/sys/validators v0.0.0-latest
- gno.land/r/gov/dao v0.0.0-latest
- gno.land/r/sys/validators v0.0.0-latest
-)
diff --git a/examples/gno.land/r/gnoland/valopers/v2/gno.mod b/examples/gno.land/r/gnoland/valopers/v2/gno.mod
new file mode 100644
index 00000000000..064fe6d811e
--- /dev/null
+++ b/examples/gno.land/r/gnoland/valopers/v2/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/gnoland/valopers/v2
diff --git a/examples/gno.land/r/gnoland/valopers/init.gno b/examples/gno.land/r/gnoland/valopers/v2/init.gno
similarity index 100%
rename from examples/gno.land/r/gnoland/valopers/init.gno
rename to examples/gno.land/r/gnoland/valopers/v2/init.gno
diff --git a/examples/gno.land/r/gnoland/valopers/valopers.gno b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno
similarity index 90%
rename from examples/gno.land/r/gnoland/valopers/valopers.gno
rename to examples/gno.land/r/gnoland/valopers/v2/valopers.gno
index 74cec941e0d..d88ea4b872f 100644
--- a/examples/gno.land/r/gnoland/valopers/valopers.gno
+++ b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno
@@ -6,10 +6,11 @@ import (
"std"
"gno.land/p/demo/avl"
+ "gno.land/p/demo/dao"
"gno.land/p/demo/ufmt"
pVals "gno.land/p/sys/validators"
- govdao "gno.land/r/gov/dao"
- "gno.land/r/sys/validators"
+ "gno.land/r/gov/dao/bridge"
+ validators "gno.land/r/sys/validators/v2"
)
const (
@@ -25,6 +26,7 @@ var valopers *avl.Tree // Address -> Valoper
// Valoper represents a validator operator profile
type Valoper struct {
Name string // the display name of the valoper
+ Moniker string // the moniker of the valoper
Description string // the description of the valoper
Address std.Address // The bech32 gno address of the validator
@@ -101,7 +103,7 @@ func Render(_ string) string {
// Render renders a single valoper with their information
func (v Valoper) Render() string {
- output := ufmt.Sprintf("## %s\n", v.Name)
+ output := ufmt.Sprintf("## %s (%s)\n", v.Name, v.Moniker)
output += ufmt.Sprintf("%s\n\n", v.Description)
output += ufmt.Sprintf("- Address: %s\n", v.Address.String())
output += ufmt.Sprintf("- PubKey: %s\n", v.PubKey)
@@ -168,14 +170,19 @@ func GovDAOProposal(address std.Address) {
// Create the executor
executor := validators.NewPropExecutor(changesFn)
- // Craft the proposal comment
- comment := ufmt.Sprintf(
- "Proposal to add valoper %s (Address: %s; PubKey: %s) to the valset",
+ // Craft the proposal description
+ description := ufmt.Sprintf(
+ "Add valoper %s (Address: %s; PubKey: %s) to the valset",
valoper.Name,
valoper.Address.String(),
valoper.PubKey,
)
+ prop := dao.ProposalRequest{
+ Description: description,
+ Executor: executor,
+ }
+
// Create the govdao proposal
- govdao.Propose(comment, executor)
+ bridge.GovDAO().Propose(prop)
}
diff --git a/examples/gno.land/r/gnoland/valopers/valopers_test.gno b/examples/gno.land/r/gnoland/valopers/v2/valopers_test.gno
similarity index 97%
rename from examples/gno.land/r/gnoland/valopers/valopers_test.gno
rename to examples/gno.land/r/gnoland/valopers/v2/valopers_test.gno
index 89544c46ee5..b5940738769 100644
--- a/examples/gno.land/r/gnoland/valopers/valopers_test.gno
+++ b/examples/gno.land/r/gnoland/valopers/v2/valopers_test.gno
@@ -38,6 +38,7 @@ func TestValopers_Register(t *testing.T) {
v := Valoper{
Address: testutils.TestAddress("valoper"),
Name: "new valoper",
+ Moniker: "val-1",
PubKey: "pub key",
}
@@ -50,6 +51,7 @@ func TestValopers_Register(t *testing.T) {
uassert.Equal(t, v.Address, valoper.Address)
uassert.Equal(t, v.Name, valoper.Name)
+ uassert.Equal(t, v.Moniker, valoper.Moniker)
uassert.Equal(t, v.PubKey, valoper.PubKey)
})
})
diff --git a/examples/gno.land/r/gov/dao/bridge/bridge.gno b/examples/gno.land/r/gov/dao/bridge/bridge.gno
new file mode 100644
index 00000000000..ba47978f33f
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/bridge/bridge.gno
@@ -0,0 +1,39 @@
+package bridge
+
+import (
+ "std"
+
+ "gno.land/p/demo/ownable"
+)
+
+const initialOwner = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") // @moul
+
+var b *Bridge
+
+// Bridge is the active GovDAO
+// implementation bridge
+type Bridge struct {
+ *ownable.Ownable
+
+ dao DAO
+}
+
+// init constructs the initial GovDAO implementation
+func init() {
+ b = &Bridge{
+ Ownable: ownable.NewWithAddress(initialOwner),
+ dao: &govdaoV2{},
+ }
+}
+
+// SetDAO sets the currently active GovDAO implementation
+func SetDAO(dao DAO) {
+ b.AssertCallerIsOwner()
+
+ b.dao = dao
+}
+
+// GovDAO returns the current GovDAO implementation
+func GovDAO() DAO {
+ return b.dao
+}
diff --git a/examples/gno.land/r/gov/dao/bridge/bridge_test.gno b/examples/gno.land/r/gov/dao/bridge/bridge_test.gno
new file mode 100644
index 00000000000..38b5d4be257
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/bridge/bridge_test.gno
@@ -0,0 +1,64 @@
+package bridge
+
+import (
+ "testing"
+
+ "std"
+
+ "gno.land/p/demo/dao"
+ "gno.land/p/demo/ownable"
+ "gno.land/p/demo/testutils"
+ "gno.land/p/demo/uassert"
+ "gno.land/p/demo/urequire"
+)
+
+func TestBridge_DAO(t *testing.T) {
+ var (
+ proposalID = uint64(10)
+ mockDAO = &mockDAO{
+ proposeFn: func(_ dao.ProposalRequest) uint64 {
+ return proposalID
+ },
+ }
+ )
+
+ b.dao = mockDAO
+
+ uassert.Equal(t, proposalID, GovDAO().Propose(dao.ProposalRequest{}))
+}
+
+func TestBridge_SetDAO(t *testing.T) {
+ t.Run("invalid owner", func(t *testing.T) {
+ // Attempt to set a new DAO implementation
+ uassert.PanicsWithMessage(t, ownable.ErrUnauthorized.Error(), func() {
+ SetDAO(&mockDAO{})
+ })
+ })
+
+ t.Run("valid owner", func(t *testing.T) {
+ var (
+ addr = testutils.TestAddress("owner")
+
+ proposalID = uint64(10)
+ mockDAO = &mockDAO{
+ proposeFn: func(_ dao.ProposalRequest) uint64 {
+ return proposalID
+ },
+ }
+ )
+
+ std.TestSetOrigCaller(addr)
+
+ b.Ownable = ownable.NewWithAddress(addr)
+
+ urequire.NotPanics(t, func() {
+ SetDAO(mockDAO)
+ })
+
+ uassert.Equal(
+ t,
+ mockDAO.Propose(dao.ProposalRequest{}),
+ GovDAO().Propose(dao.ProposalRequest{}),
+ )
+ })
+}
diff --git a/examples/gno.land/r/gov/dao/bridge/doc.gno b/examples/gno.land/r/gov/dao/bridge/doc.gno
new file mode 100644
index 00000000000..f812b3c0787
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/bridge/doc.gno
@@ -0,0 +1,4 @@
+// Package bridge represents a GovDAO implementation wrapper, used by other Realms and Packages to
+// always fetch the most active GovDAO implementation, instead of directly referencing it, and having to
+// update it each time the GovDAO implementation changes
+package bridge
diff --git a/examples/gno.land/r/gov/dao/bridge/gno.mod b/examples/gno.land/r/gov/dao/bridge/gno.mod
new file mode 100644
index 00000000000..9f472eaa464
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/bridge/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/gov/dao/bridge
diff --git a/examples/gno.land/r/gov/dao/bridge/mock_test.gno b/examples/gno.land/r/gov/dao/bridge/mock_test.gno
new file mode 100644
index 00000000000..05ac430b4c4
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/bridge/mock_test.gno
@@ -0,0 +1,68 @@
+package bridge
+
+import (
+ "gno.land/p/demo/dao"
+ "gno.land/p/demo/membstore"
+)
+
+type (
+ proposeDelegate func(dao.ProposalRequest) uint64
+ voteOnProposalDelegate func(uint64, dao.VoteOption)
+ executeProposalDelegate func(uint64)
+ getPropStoreDelegate func() dao.PropStore
+ getMembStoreDelegate func() membstore.MemberStore
+ newGovDAOExecutorDelegate func(func() error) dao.Executor
+)
+
+type mockDAO struct {
+ proposeFn proposeDelegate
+ voteOnProposalFn voteOnProposalDelegate
+ executeProposalFn executeProposalDelegate
+ getPropStoreFn getPropStoreDelegate
+ getMembStoreFn getMembStoreDelegate
+ newGovDAOExecutorFn newGovDAOExecutorDelegate
+}
+
+func (m *mockDAO) Propose(request dao.ProposalRequest) uint64 {
+ if m.proposeFn != nil {
+ return m.proposeFn(request)
+ }
+
+ return 0
+}
+
+func (m *mockDAO) VoteOnProposal(id uint64, option dao.VoteOption) {
+ if m.voteOnProposalFn != nil {
+ m.voteOnProposalFn(id, option)
+ }
+}
+
+func (m *mockDAO) ExecuteProposal(id uint64) {
+ if m.executeProposalFn != nil {
+ m.executeProposalFn(id)
+ }
+}
+
+func (m *mockDAO) GetPropStore() dao.PropStore {
+ if m.getPropStoreFn != nil {
+ return m.getPropStoreFn()
+ }
+
+ return nil
+}
+
+func (m *mockDAO) GetMembStore() membstore.MemberStore {
+ if m.getMembStoreFn != nil {
+ return m.getMembStoreFn()
+ }
+
+ return nil
+}
+
+func (m *mockDAO) NewGovDAOExecutor(cb func() error) dao.Executor {
+ if m.newGovDAOExecutorFn != nil {
+ return m.newGovDAOExecutorFn(cb)
+ }
+
+ return nil
+}
diff --git a/examples/gno.land/r/gov/dao/bridge/types.gno b/examples/gno.land/r/gov/dao/bridge/types.gno
new file mode 100644
index 00000000000..27ea8fb62d4
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/bridge/types.gno
@@ -0,0 +1,17 @@
+package bridge
+
+import (
+ "gno.land/p/demo/dao"
+ "gno.land/p/demo/membstore"
+)
+
+// DAO abstracts the commonly used DAO interface
+type DAO interface {
+ Propose(dao.ProposalRequest) uint64
+ VoteOnProposal(uint64, dao.VoteOption)
+ ExecuteProposal(uint64)
+ GetPropStore() dao.PropStore
+ GetMembStore() membstore.MemberStore
+
+ NewGovDAOExecutor(func() error) dao.Executor
+}
diff --git a/examples/gno.land/r/gov/dao/bridge/v2.gno b/examples/gno.land/r/gov/dao/bridge/v2.gno
new file mode 100644
index 00000000000..216419cf31d
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/bridge/v2.gno
@@ -0,0 +1,42 @@
+package bridge
+
+import (
+ "gno.land/p/demo/dao"
+ "gno.land/p/demo/membstore"
+ govdao "gno.land/r/gov/dao/v2"
+)
+
+// govdaoV2 is a wrapper for interacting with the /r/gov/dao/v2 Realm
+type govdaoV2 struct{}
+
+func (g *govdaoV2) Propose(request dao.ProposalRequest) uint64 {
+ return govdao.Propose(request)
+}
+
+func (g *govdaoV2) VoteOnProposal(id uint64, option dao.VoteOption) {
+ govdao.VoteOnProposal(id, option)
+}
+
+func (g *govdaoV2) ExecuteProposal(id uint64) {
+ govdao.ExecuteProposal(id)
+}
+
+func (g *govdaoV2) GetPropStore() dao.PropStore {
+ return govdao.GetPropStore()
+}
+
+func (g *govdaoV2) GetMembStore() membstore.MemberStore {
+ return govdao.GetMembStore()
+}
+
+func (g *govdaoV2) NewGovDAOExecutor(cb func() error) dao.Executor {
+ return govdao.NewGovDAOExecutor(cb)
+}
+
+func (g *govdaoV2) NewMemberPropExecutor(cb func() []membstore.Member) dao.Executor {
+ return govdao.NewMemberPropExecutor(cb)
+}
+
+func (g *govdaoV2) NewMembStoreImplExecutor(cb func() membstore.MemberStore) dao.Executor {
+ return govdao.NewMembStoreImplExecutor(cb)
+}
diff --git a/examples/gno.land/r/gov/dao/dao.gno b/examples/gno.land/r/gov/dao/dao.gno
deleted file mode 100644
index 632935dafed..00000000000
--- a/examples/gno.land/r/gov/dao/dao.gno
+++ /dev/null
@@ -1,207 +0,0 @@
-package govdao
-
-import (
- "std"
- "strconv"
-
- "gno.land/p/demo/ufmt"
- pproposal "gno.land/p/gov/proposal"
-)
-
-var (
- proposals = make([]*proposal, 0)
- members = make([]std.Address, 0) // XXX: these should be pointers to avoid data duplication. Not possible due to VM bugs
-)
-
-const (
- msgMissingExecutor = "missing proposal executor"
- msgPropExecuted = "prop already executed"
- msgPropExpired = "prop is expired"
- msgPropInactive = "prop is not active anymore"
- msgPropActive = "prop is still active"
- msgPropNotAccepted = "prop is not accepted"
-
- msgCallerNotAMember = "caller is not member of govdao"
- msgProposalNotFound = "proposal not found"
-)
-
-type proposal struct {
- author std.Address
- comment string
- executor pproposal.Executor
- voter Voter
- executed bool
- voters []std.Address // XXX: these should be pointers to avoid data duplication. Not possible due to VM bugs.
-}
-
-func (p proposal) Status() Status {
- if p.executor.IsExpired() {
- return Expired
- }
-
- if p.executor.IsDone() {
- return Succeeded
- }
-
- if !p.voter.IsFinished(members) {
- return Active
- }
-
- if p.voter.IsAccepted(members) {
- return Accepted
- }
-
- return NotAccepted
-}
-
-// Propose is designed to be called by another contract or with
-// `maketx run`, not by a `maketx call`.
-func Propose(comment string, executor pproposal.Executor) int {
- // XXX: require payment?
- if executor == nil {
- panic(msgMissingExecutor)
- }
- caller := std.GetOrigCaller() // XXX: CHANGE THIS WHEN MSGRUN PERSIST CODE ESCAPING THE main() SCOPE! IT IS UNSAFE!
- AssertIsMember(caller)
-
- prop := &proposal{
- comment: comment,
- executor: executor,
- author: caller,
- voter: NewPercentageVoter(66), // at least 2/3 must say yes
- }
-
- proposals = append(proposals, prop)
-
- return len(proposals) - 1
-}
-
-func VoteOnProposal(idx int, option string) {
- assertProposalExists(idx)
- caller := std.GetOrigCaller() // XXX: CHANGE THIS WHEN MSGRUN PERSIST CODE ESCAPING THE main() SCOPE! IT IS UNSAFE!
- AssertIsMember(caller)
-
- prop := getProposal(idx)
-
- if prop.executed {
- panic(msgPropExecuted)
- }
-
- if prop.executor.IsExpired() {
- panic(msgPropExpired)
- }
-
- if prop.voter.IsFinished(members) {
- panic(msgPropInactive)
- }
-
- prop.voter.Vote(members, caller, option)
-}
-
-func ExecuteProposal(idx int) {
- assertProposalExists(idx)
- prop := getProposal(idx)
-
- if prop.executed {
- panic(msgPropExecuted)
- }
-
- if prop.executor.IsExpired() {
- panic(msgPropExpired)
- }
-
- if !prop.voter.IsFinished(members) {
- panic(msgPropActive)
- }
-
- if !prop.voter.IsAccepted(members) {
- panic(msgPropNotAccepted)
- }
-
- prop.executor.Execute()
- prop.voters = members
- prop.executed = true
-}
-
-func IsMember(addr std.Address) bool {
- if len(members) == 0 { // special case for initial execution
- return true
- }
-
- for _, v := range members {
- if v == addr {
- return true
- }
- }
-
- return false
-}
-
-func AssertIsMember(addr std.Address) {
- if !IsMember(addr) {
- panic(msgCallerNotAMember)
- }
-}
-
-func Render(path string) string {
- if path == "" {
- if len(proposals) == 0 {
- return "No proposals found :(" // corner case
- }
-
- output := ""
- for idx, prop := range proposals {
- output += ufmt.Sprintf("- [%d](/r/gov/dao:%d) - %s (**%s**)(by %s)\n", idx, idx, prop.comment, string(prop.Status()), prop.author)
- }
-
- return output
- }
-
- // else display the proposal
- idx, err := strconv.Atoi(path)
- if err != nil {
- return "404"
- }
-
- if !proposalExists(idx) {
- return "404"
- }
- prop := getProposal(idx)
-
- vs := members
- if prop.executed {
- vs = prop.voters
- }
-
- output := ""
- output += ufmt.Sprintf("# Prop #%d", idx)
- output += "\n\n"
- output += prop.comment
- output += "\n\n"
- output += ufmt.Sprintf("Status: %s", string(prop.Status()))
- output += "\n\n"
- output += ufmt.Sprintf("Voting status: %s", prop.voter.Status(vs))
- output += "\n\n"
- output += ufmt.Sprintf("Author: %s", string(prop.author))
- output += "\n\n"
-
- return output
-}
-
-func getProposal(idx int) *proposal {
- if idx > len(proposals)-1 {
- panic(msgProposalNotFound)
- }
-
- return proposals[idx]
-}
-
-func proposalExists(idx int) bool {
- return idx >= 0 && idx <= len(proposals)
-}
-
-func assertProposalExists(idx int) {
- if !proposalExists(idx) {
- panic("invalid proposal id")
- }
-}
diff --git a/examples/gno.land/r/gov/dao/dao_test.gno b/examples/gno.land/r/gov/dao/dao_test.gno
deleted file mode 100644
index 96eaba7f5e9..00000000000
--- a/examples/gno.land/r/gov/dao/dao_test.gno
+++ /dev/null
@@ -1,192 +0,0 @@
-package govdao
-
-import (
- "std"
- "testing"
-
- "gno.land/p/demo/testutils"
- "gno.land/p/demo/urequire"
- pproposal "gno.land/p/gov/proposal"
-)
-
-func TestPackage(t *testing.T) {
- u1 := testutils.TestAddress("u1")
- u2 := testutils.TestAddress("u2")
- u3 := testutils.TestAddress("u3")
-
- members = append(members, u1)
- members = append(members, u2)
- members = append(members, u3)
-
- nu1 := testutils.TestAddress("random1")
-
- out := Render("")
-
- expected := "No proposals found :("
- urequire.Equal(t, expected, out)
-
- var called bool
- ex := pproposal.NewExecutor(func() error {
- called = true
- return nil
- })
-
- std.TestSetOrigCaller(u1)
- pid := Propose("dummy proposal", ex)
-
- // try to vote not being a member
- std.TestSetOrigCaller(nu1)
-
- urequire.PanicsWithMessage(t, msgCallerNotAMember, func() {
- VoteOnProposal(pid, "YES")
- })
-
- // try to vote several times
- std.TestSetOrigCaller(u1)
- urequire.NotPanics(t, func() {
- VoteOnProposal(pid, "YES")
- })
- urequire.PanicsWithMessage(t, msgAlreadyVoted, func() {
- VoteOnProposal(pid, "YES")
- })
-
- out = Render("0")
- expected = `# Prop #0
-
-dummy proposal
-
-Status: active
-
-Voting status: YES: 1, NO: 0, percent: 33, members: 3
-
-Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr
-
-`
-
- urequire.Equal(t, expected, out)
-
- std.TestSetOrigCaller(u2)
- urequire.PanicsWithMessage(t, msgWrongVotingValue, func() {
- VoteOnProposal(pid, "INCORRECT")
- })
- urequire.NotPanics(t, func() {
- VoteOnProposal(pid, "NO")
- })
-
- out = Render("0")
- expected = `# Prop #0
-
-dummy proposal
-
-Status: active
-
-Voting status: YES: 1, NO: 1, percent: 33, members: 3
-
-Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr
-
-`
-
- urequire.Equal(t, expected, out)
-
- std.TestSetOrigCaller(u3)
- urequire.NotPanics(t, func() {
- VoteOnProposal(pid, "YES")
- })
-
- out = Render("0")
- expected = `# Prop #0
-
-dummy proposal
-
-Status: accepted
-
-Voting status: YES: 2, NO: 1, percent: 66, members: 3
-
-Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr
-
-`
-
- urequire.Equal(t, expected, out)
-
- // Add a new member, so non-executed proposals will change the voting status
- u4 := testutils.TestAddress("u4")
- members = append(members, u4)
-
- out = Render("0")
- expected = `# Prop #0
-
-dummy proposal
-
-Status: active
-
-Voting status: YES: 2, NO: 1, percent: 50, members: 4
-
-Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr
-
-`
-
- urequire.Equal(t, expected, out)
-
- std.TestSetOrigCaller(u4)
- urequire.NotPanics(t, func() {
- VoteOnProposal(pid, "YES")
- })
-
- out = Render("0")
- expected = `# Prop #0
-
-dummy proposal
-
-Status: accepted
-
-Voting status: YES: 3, NO: 1, percent: 75, members: 4
-
-Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr
-
-`
-
- urequire.Equal(t, expected, out)
-
- ExecuteProposal(pid)
- urequire.True(t, called)
-
- out = Render("0")
- expected = `# Prop #0
-
-dummy proposal
-
-Status: succeeded
-
-Voting status: YES: 3, NO: 1, percent: 75, members: 4
-
-Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr
-
-`
-
- urequire.Equal(t, expected, out)
-
- // Add a new member and try to vote an already executed proposal
- u5 := testutils.TestAddress("u5")
- members = append(members, u5)
- std.TestSetOrigCaller(u5)
- urequire.PanicsWithMessage(t, msgPropExecuted, func() {
- ExecuteProposal(pid)
- })
-
- // even if we added a new member the executed proposal is showing correctly the members that voted on it
- out = Render("0")
- expected = `# Prop #0
-
-dummy proposal
-
-Status: succeeded
-
-Voting status: YES: 3, NO: 1, percent: 75, members: 4
-
-Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr
-
-`
-
- urequire.Equal(t, expected, out)
-
-}
diff --git a/examples/gno.land/r/gov/dao/gno.mod b/examples/gno.land/r/gov/dao/gno.mod
deleted file mode 100644
index f3c0bae990e..00000000000
--- a/examples/gno.land/r/gov/dao/gno.mod
+++ /dev/null
@@ -1,8 +0,0 @@
-module gno.land/r/gov/dao
-
-require (
- gno.land/p/demo/testutils v0.0.0-latest
- gno.land/p/demo/ufmt v0.0.0-latest
- gno.land/p/demo/urequire v0.0.0-latest
- gno.land/p/gov/proposal v0.0.0-latest
-)
diff --git a/examples/gno.land/r/gov/dao/memberset.gno b/examples/gno.land/r/gov/dao/memberset.gno
deleted file mode 100644
index 3abd52ae99d..00000000000
--- a/examples/gno.land/r/gov/dao/memberset.gno
+++ /dev/null
@@ -1,40 +0,0 @@
-package govdao
-
-import (
- "std"
-
- pproposal "gno.land/p/gov/proposal"
-)
-
-const daoPkgPath = "gno.land/r/gov/dao"
-
-const (
- errNoChangesProposed = "no set changes proposed"
- errNotGovDAO = "caller not govdao executor"
-)
-
-func NewPropExecutor(changesFn func() []std.Address) pproposal.Executor {
- if changesFn == nil {
- panic(errNoChangesProposed)
- }
-
- callback := func() error {
- // Make sure the GovDAO executor runs the valset changes
- assertGovDAOCaller()
-
- for _, addr := range changesFn() {
- members = append(members, addr)
- }
-
- return nil
- }
-
- return pproposal.NewExecutor(callback)
-}
-
-// assertGovDAOCaller verifies the caller is the GovDAO executor
-func assertGovDAOCaller() {
- if std.CurrentRealm().PkgPath() != daoPkgPath {
- panic(errNotGovDAO)
- }
-}
diff --git a/examples/gno.land/r/gov/dao/prop1_filetest.gno b/examples/gno.land/r/gov/dao/prop1_filetest.gno
deleted file mode 100644
index 49a200fd561..00000000000
--- a/examples/gno.land/r/gov/dao/prop1_filetest.gno
+++ /dev/null
@@ -1,131 +0,0 @@
-// Please note that this package is intended for demonstration purposes only.
-// You could execute this code (the init part) by running a `maketx run` command
-// or by uploading a similar package to a personal namespace.
-//
-// For the specific case of validators, a `r/gnoland/valopers` will be used to
-// organize the lifecycle of validators (register, etc), and this more complex
-// contract will be responsible to generate proposals.
-package main
-
-import (
- "std"
-
- pVals "gno.land/p/sys/validators"
- govdao "gno.land/r/gov/dao"
- "gno.land/r/sys/validators"
-)
-
-const daoPkgPath = "gno.land/r/gov/dao"
-
-func init() {
- membersFn := func() []std.Address {
- return []std.Address{
- std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"),
- }
- }
-
- mExec := govdao.NewPropExecutor(membersFn)
-
- comment := "adding someone to vote"
- id := govdao.Propose(comment, mExec)
- govdao.ExecuteProposal(id)
-
- changesFn := func() []pVals.Validator {
- return []pVals.Validator{
- {
- Address: std.Address("g12345678"),
- PubKey: "pubkey",
- VotingPower: 10, // add a new validator
- },
- {
- Address: std.Address("g000000000"),
- PubKey: "pubkey",
- VotingPower: 10, // add a new validator
- },
- {
- Address: std.Address("g000000000"),
- PubKey: "pubkey",
- VotingPower: 0, // remove an existing validator
- },
- }
- }
-
- // Wraps changesFn to emit a certified event only if executed from a
- // complete governance proposal process.
- executor := validators.NewPropExecutor(changesFn)
-
- // Create a proposal.
- // XXX: payment
- comment = "manual valset changes proposal example"
- govdao.Propose(comment, executor)
-}
-
-func main() {
- println("--")
- println(govdao.Render(""))
- println("--")
- println(govdao.Render("1"))
- println("--")
- govdao.VoteOnProposal(1, "YES")
- println("--")
- println(govdao.Render("1"))
- println("--")
- println(validators.Render(""))
- println("--")
- govdao.ExecuteProposal(1)
- println("--")
- println(govdao.Render("1"))
- println("--")
- println(validators.Render(""))
-}
-
-// Output:
-// --
-// - [0](/r/gov/dao:0) - adding someone to vote (**succeeded**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
-// - [1](/r/gov/dao:1) - manual valset changes proposal example (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
-//
-// --
-// # Prop #1
-//
-// manual valset changes proposal example
-//
-// Status: active
-//
-// Voting status: YES: 0, NO: 0, percent: 0, members: 1
-//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
-//
-//
-// --
-// --
-// # Prop #1
-//
-// manual valset changes proposal example
-//
-// Status: accepted
-//
-// Voting status: YES: 1, NO: 0, percent: 100, members: 1
-//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
-//
-//
-// --
-// No valset changes to apply.
-// --
-// --
-// # Prop #1
-//
-// manual valset changes proposal example
-//
-// Status: succeeded
-//
-// Voting status: YES: 1, NO: 0, percent: 100, members: 1
-//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
-//
-//
-// --
-// Valset changes:
-// - #123: g12345678 (10)
-// - #123: g000000000 (10)
-// - #123: g000000000 (0)
diff --git a/examples/gno.land/r/gov/dao/prop2_filetest.gno b/examples/gno.land/r/gov/dao/prop2_filetest.gno
deleted file mode 100644
index 047709cc45f..00000000000
--- a/examples/gno.land/r/gov/dao/prop2_filetest.gno
+++ /dev/null
@@ -1,120 +0,0 @@
-package main
-
-import (
- "std"
- "time"
-
- "gno.land/p/demo/context"
- "gno.land/p/gov/proposal"
- gnoblog "gno.land/r/gnoland/blog"
- govdao "gno.land/r/gov/dao"
-)
-
-func init() {
- membersFn := func() []std.Address {
- return []std.Address{
- std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"),
- }
- }
-
- mExec := govdao.NewPropExecutor(membersFn)
-
- comment := "adding someone to vote"
-
- id := govdao.Propose(comment, mExec)
-
- govdao.ExecuteProposal(id)
-
- executor := proposal.NewCtxExecutor(func(ctx context.Context) error {
- gnoblog.DaoAddPost(
- ctx,
- "hello-from-govdao", // slug
- "Hello from GovDAO!", // title
- "This post was published by a GovDAO proposal.", // body
- time.Now().Format(time.RFC3339), // publidation date
- "moul", // authors
- "govdao,example", // tags
- )
- return nil
- })
-
- // Create a proposal.
- // XXX: payment
- comment = "post a new blogpost about govdao"
- govdao.Propose(comment, executor)
-}
-
-func main() {
- println("--")
- println(govdao.Render(""))
- println("--")
- println(govdao.Render("1"))
- println("--")
- govdao.VoteOnProposal(1, "YES")
- println("--")
- println(govdao.Render("1"))
- println("--")
- println(gnoblog.Render(""))
- println("--")
- govdao.ExecuteProposal(1)
- println("--")
- println(govdao.Render("1"))
- println("--")
- println(gnoblog.Render(""))
-}
-
-// Output:
-// --
-// - [0](/r/gov/dao:0) - adding someone to vote (**succeeded**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
-// - [1](/r/gov/dao:1) - post a new blogpost about govdao (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
-//
-// --
-// # Prop #1
-//
-// post a new blogpost about govdao
-//
-// Status: active
-//
-// Voting status: YES: 0, NO: 0, percent: 0, members: 1
-//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
-//
-//
-// --
-// --
-// # Prop #1
-//
-// post a new blogpost about govdao
-//
-// Status: accepted
-//
-// Voting status: YES: 1, NO: 0, percent: 100, members: 1
-//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
-//
-//
-// --
-// # Gnoland's Blog
-//
-// No posts.
-// --
-// --
-// # Prop #1
-//
-// post a new blogpost about govdao
-//
-// Status: succeeded
-//
-// Voting status: YES: 1, NO: 0, percent: 100, members: 1
-//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
-//
-//
-// --
-// # Gnoland's Blog
-//
-//
-//
-// ### [Hello from GovDAO!](/r/gnoland/blog:p/hello-from-govdao)
-// 13 Feb 2009
-//
diff --git a/examples/gno.land/r/gov/dao/types.gno b/examples/gno.land/r/gov/dao/types.gno
deleted file mode 100644
index 123fc489075..00000000000
--- a/examples/gno.land/r/gov/dao/types.gno
+++ /dev/null
@@ -1,32 +0,0 @@
-package govdao
-
-import (
- "std"
-)
-
-// Status enum.
-type Status string
-
-var (
- Accepted Status = "accepted"
- Active Status = "active"
- NotAccepted Status = "not accepted"
- Expired Status = "expired"
- Succeeded Status = "succeeded"
-)
-
-// Voter defines the needed methods for a voting system
-type Voter interface {
-
- // IsAccepted indicates if the voting process had been accepted
- IsAccepted(voters []std.Address) bool
-
- // IsFinished indicates if the voting process is finished
- IsFinished(voters []std.Address) bool
-
- // Vote adds a new vote to the voting system
- Vote(voters []std.Address, caller std.Address, flag string)
-
- // Status returns a human friendly string describing how the voting process is going
- Status(voters []std.Address) string
-}
diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno
new file mode 100644
index 00000000000..5ee8e63236a
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/v2/dao.gno
@@ -0,0 +1,67 @@
+package govdao
+
+import (
+ "std"
+
+ "gno.land/p/demo/dao"
+ "gno.land/p/demo/membstore"
+ "gno.land/p/demo/simpledao"
+)
+
+var (
+ d *simpledao.SimpleDAO // the current active DAO implementation
+ members membstore.MemberStore // the member store
+)
+
+const daoPkgPath = "gno.land/r/gov/dao/v2"
+
+func init() {
+ // Example initial member set (just test addresses)
+ set := []membstore.Member{
+ {
+ Address: std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"),
+ VotingPower: 10,
+ },
+ }
+
+ // Set the member store
+ members = membstore.NewMembStore(membstore.WithInitialMembers(set), membstore.WithDAOPkgPath(daoPkgPath))
+
+ // Set the DAO implementation
+ d = simpledao.New(members)
+}
+
+// Propose is designed to be called by another contract or with
+// `maketx run`, not by a `maketx call`.
+func Propose(request dao.ProposalRequest) uint64 {
+ idx, err := d.Propose(request)
+ if err != nil {
+ panic(err)
+ }
+
+ return idx
+}
+
+// VoteOnProposal casts a vote for the given proposal
+func VoteOnProposal(id uint64, option dao.VoteOption) {
+ if err := d.VoteOnProposal(id, option); err != nil {
+ panic(err)
+ }
+}
+
+// ExecuteProposal executes the proposal
+func ExecuteProposal(id uint64) {
+ if err := d.ExecuteProposal(id); err != nil {
+ panic(err)
+ }
+}
+
+// GetPropStore returns the active proposal store
+func GetPropStore() dao.PropStore {
+ return d
+}
+
+// GetMembStore returns the active member store
+func GetMembStore() membstore.MemberStore {
+ return members
+}
diff --git a/examples/gno.land/r/gov/dao/v2/gno.mod b/examples/gno.land/r/gov/dao/v2/gno.mod
new file mode 100644
index 00000000000..4daf8c600a1
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/v2/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/gov/dao/v2
diff --git a/examples/gno.land/r/gov/dao/v2/poc.gno b/examples/gno.land/r/gov/dao/v2/poc.gno
new file mode 100644
index 00000000000..30d8a403f6e
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/v2/poc.gno
@@ -0,0 +1,92 @@
+package govdao
+
+import (
+ "errors"
+ "std"
+
+ "gno.land/p/demo/combinederr"
+ "gno.land/p/demo/dao"
+ "gno.land/p/demo/membstore"
+ "gno.land/p/gov/executor"
+)
+
+var errNoChangesProposed = errors.New("no set changes proposed")
+
+// NewGovDAOExecutor creates the govdao wrapped callback executor
+func NewGovDAOExecutor(cb func() error) dao.Executor {
+ if cb == nil {
+ panic(errNoChangesProposed)
+ }
+
+ return executor.NewCallbackExecutor(
+ cb,
+ std.CurrentRealm().PkgPath(),
+ )
+}
+
+// NewMemberPropExecutor returns the GOVDAO member change executor
+func NewMemberPropExecutor(changesFn func() []membstore.Member) dao.Executor {
+ if changesFn == nil {
+ panic(errNoChangesProposed)
+ }
+
+ callback := func() error {
+ errs := &combinederr.CombinedError{}
+ cbMembers := changesFn()
+
+ for _, member := range cbMembers {
+ switch {
+ case !members.IsMember(member.Address):
+ // Addition request
+ err := members.AddMember(member)
+
+ errs.Add(err)
+ case member.VotingPower == 0:
+ // Remove request
+ err := members.UpdateMember(member.Address, membstore.Member{
+ Address: member.Address,
+ VotingPower: 0, // 0 indicated removal
+ })
+
+ errs.Add(err)
+ default:
+ // Update request
+ err := members.UpdateMember(member.Address, member)
+
+ errs.Add(err)
+ }
+ }
+
+ // Check if there were any execution errors
+ if errs.Size() == 0 {
+ return nil
+ }
+
+ return errs
+ }
+
+ return NewGovDAOExecutor(callback)
+}
+
+func NewMembStoreImplExecutor(changeFn func() membstore.MemberStore) dao.Executor {
+ if changeFn == nil {
+ panic(errNoChangesProposed)
+ }
+
+ callback := func() error {
+ setMembStoreImpl(changeFn())
+
+ return nil
+ }
+
+ return NewGovDAOExecutor(callback)
+}
+
+// setMembStoreImpl sets a new dao.MembStore implementation
+func setMembStoreImpl(impl membstore.MemberStore) {
+ if impl == nil {
+ panic("invalid member store")
+ }
+
+ members = impl
+}
diff --git a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno
new file mode 100644
index 00000000000..7d8975e1fe8
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno
@@ -0,0 +1,259 @@
+// Please note that this package is intended for demonstration purposes only.
+// You could execute this code (the init part) by running a `maketx run` command
+// or by uploading a similar package to a personal namespace.
+//
+// For the specific case of validators, a `r/gnoland/valopers` will be used to
+// organize the lifecycle of validators (register, etc), and this more complex
+// contract will be responsible to generate proposals.
+package main
+
+import (
+ "std"
+
+ "gno.land/p/demo/dao"
+ pVals "gno.land/p/sys/validators"
+ govdao "gno.land/r/gov/dao/v2"
+ validators "gno.land/r/sys/validators/v2"
+)
+
+func init() {
+ changesFn := func() []pVals.Validator {
+ return []pVals.Validator{
+ {
+ Address: std.Address("g12345678"),
+ PubKey: "pubkey",
+ VotingPower: 10, // add a new validator
+ },
+ {
+ Address: std.Address("g000000000"),
+ PubKey: "pubkey",
+ VotingPower: 10, // add a new validator
+ },
+ {
+ Address: std.Address("g000000000"),
+ PubKey: "pubkey",
+ VotingPower: 0, // remove an existing validator
+ },
+ }
+ }
+
+ // Wraps changesFn to emit a certified event only if executed from a
+ // complete governance proposal process.
+ executor := validators.NewPropExecutor(changesFn)
+
+ // Create a proposal
+ title := "Valset change"
+ description := "manual valset changes proposal example"
+
+ prop := dao.ProposalRequest{
+ Title: title,
+ Description: description,
+ Executor: executor,
+ }
+
+ govdao.Propose(prop)
+}
+
+func main() {
+ println("--")
+ println(govdao.Render(""))
+ println("--")
+ println(govdao.Render("0"))
+ println("--")
+ govdao.VoteOnProposal(0, dao.YesVote)
+ println("--")
+ println(govdao.Render("0"))
+ println("--")
+ println(validators.Render(""))
+ println("--")
+ govdao.ExecuteProposal(0)
+ println("--")
+ println(govdao.Render("0"))
+ println("--")
+ println(validators.Render(""))
+}
+
+// Output:
+// --
+// # GovDAO Proposals
+//
+// ## [Prop #0 - Valset change](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+//
+// --
+// # Proposal #0 - Valset change
+//
+// ## Description
+//
+// manual valset changes proposal example
+//
+// ## Proposal information
+//
+// **Status: ACTIVE**
+//
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
+//
+//
+// **Threshold met: FALSE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
+//
+//
+// --
+// --
+// # Proposal #0 - Valset change
+//
+// ## Description
+//
+// manual valset changes proposal example
+//
+// ## Proposal information
+//
+// **Status: ACCEPTED**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
+//
+//
+// --
+// No valset changes to apply.
+// --
+// --
+// # Proposal #0 - Valset change
+//
+// ## Description
+//
+// manual valset changes proposal example
+//
+// ## Proposal information
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
+//
+//
+// --
+// Valset changes:
+// - #123: g12345678 (10)
+// - #123: g000000000 (10)
+// - #123: g000000000 (0)
+//
+
+// Events:
+// [
+// {
+// "type": "ProposalAdded",
+// "attrs": [
+// {
+// "key": "proposal-id",
+// "value": "0"
+// },
+// {
+// "key": "proposal-author",
+// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"
+// }
+// ],
+// "pkg_path": "gno.land/r/gov/dao/v2",
+// "func": "EmitProposalAdded"
+// },
+// {
+// "type": "VoteAdded",
+// "attrs": [
+// {
+// "key": "proposal-id",
+// "value": "0"
+// },
+// {
+// "key": "author",
+// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"
+// },
+// {
+// "key": "option",
+// "value": "YES"
+// }
+// ],
+// "pkg_path": "gno.land/r/gov/dao/v2",
+// "func": "EmitVoteAdded"
+// },
+// {
+// "type": "ProposalAccepted",
+// "attrs": [
+// {
+// "key": "proposal-id",
+// "value": "0"
+// }
+// ],
+// "pkg_path": "gno.land/r/gov/dao/v2",
+// "func": "EmitProposalAccepted"
+// },
+// {
+// "type": "ValidatorAdded",
+// "attrs": [],
+// "pkg_path": "gno.land/r/sys/validators/v2",
+// "func": "addValidator"
+// },
+// {
+// "type": "ValidatorAdded",
+// "attrs": [],
+// "pkg_path": "gno.land/r/sys/validators/v2",
+// "func": "addValidator"
+// },
+// {
+// "type": "ValidatorRemoved",
+// "attrs": [],
+// "pkg_path": "gno.land/r/sys/validators/v2",
+// "func": "removeValidator"
+// },
+// {
+// "type": "ProposalExecuted",
+// "attrs": [
+// {
+// "key": "proposal-id",
+// "value": "0"
+// },
+// {
+// "key": "exec-status",
+// "value": "accepted"
+// }
+// ],
+// "pkg_path": "gno.land/r/gov/dao/v2",
+// "func": "ExecuteProposal"
+// }
+// ]
diff --git a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno
new file mode 100644
index 00000000000..84a64bc4ee2
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno
@@ -0,0 +1,222 @@
+package main
+
+import (
+ "time"
+
+ "gno.land/p/demo/dao"
+ gnoblog "gno.land/r/gnoland/blog"
+ govdao "gno.land/r/gov/dao/v2"
+)
+
+func init() {
+ ex := gnoblog.NewPostExecutor(
+ "hello-from-govdao", // slug
+ "Hello from GovDAO!", // title
+ "This post was published by a GovDAO proposal.", // body
+ time.Now().Format(time.RFC3339), // publication date
+ "moul", // authors
+ "govdao,example", // tags
+ )
+
+ // Create a proposal
+ title := "govdao blog post title"
+ description := "post a new blogpost about govdao"
+
+ prop := dao.ProposalRequest{
+ Title: title,
+ Description: description,
+ Executor: ex,
+ }
+
+ govdao.Propose(prop)
+}
+
+func main() {
+ println("--")
+ println(govdao.Render(""))
+ println("--")
+ println(govdao.Render("0"))
+ println("--")
+ govdao.VoteOnProposal(0, "YES")
+ println("--")
+ println(govdao.Render("0"))
+ println("--")
+ println(gnoblog.Render(""))
+ println("--")
+ govdao.ExecuteProposal(0)
+ println("--")
+ println(govdao.Render("0"))
+ println("--")
+ println(gnoblog.Render(""))
+}
+
+// Output:
+// --
+// # GovDAO Proposals
+//
+// ## [Prop #0 - govdao blog post title](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+//
+// --
+// # Proposal #0 - govdao blog post title
+//
+// ## Description
+//
+// post a new blogpost about govdao
+//
+// ## Proposal information
+//
+// **Status: ACTIVE**
+//
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
+//
+//
+// **Threshold met: FALSE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
+//
+//
+// --
+// --
+// # Proposal #0 - govdao blog post title
+//
+// ## Description
+//
+// post a new blogpost about govdao
+//
+// ## Proposal information
+//
+// **Status: ACCEPTED**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
+//
+//
+// --
+// # gno.land's blog
+//
+// No posts.
+// --
+// --
+// # Proposal #0 - govdao blog post title
+//
+// ## Description
+//
+// post a new blogpost about govdao
+//
+// ## Proposal information
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
+//
+//
+// --
+// # gno.land's blog
+//
+//
+//
+// ### [Hello from GovDAO!](/r/gnoland/blog:p/hello-from-govdao)
+// 13 Feb 2009
+//
+
+// Events:
+// [
+// {
+// "type": "ProposalAdded",
+// "attrs": [
+// {
+// "key": "proposal-id",
+// "value": "0"
+// },
+// {
+// "key": "proposal-author",
+// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"
+// }
+// ],
+// "pkg_path": "gno.land/r/gov/dao/v2",
+// "func": "EmitProposalAdded"
+// },
+// {
+// "type": "VoteAdded",
+// "attrs": [
+// {
+// "key": "proposal-id",
+// "value": "0"
+// },
+// {
+// "key": "author",
+// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"
+// },
+// {
+// "key": "option",
+// "value": "YES"
+// }
+// ],
+// "pkg_path": "gno.land/r/gov/dao/v2",
+// "func": "EmitVoteAdded"
+// },
+// {
+// "type": "ProposalAccepted",
+// "attrs": [
+// {
+// "key": "proposal-id",
+// "value": "0"
+// }
+// ],
+// "pkg_path": "gno.land/r/gov/dao/v2",
+// "func": "EmitProposalAccepted"
+// },
+// {
+// "type": "ProposalExecuted",
+// "attrs": [
+// {
+// "key": "proposal-id",
+// "value": "0"
+// },
+// {
+// "key": "exec-status",
+// "value": "accepted"
+// }
+// ],
+// "pkg_path": "gno.land/r/gov/dao/v2",
+// "func": "ExecuteProposal"
+// }
+// ]
diff --git a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno
new file mode 100644
index 00000000000..068f520e7e2
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno
@@ -0,0 +1,247 @@
+package main
+
+import (
+ "std"
+
+ "gno.land/p/demo/dao"
+ "gno.land/p/demo/membstore"
+ "gno.land/r/gov/dao/bridge"
+ govdao "gno.land/r/gov/dao/v2"
+)
+
+func init() {
+ memberFn := func() []membstore.Member {
+ return []membstore.Member{
+ {
+ Address: std.Address("g123"),
+ VotingPower: 10,
+ },
+ {
+ Address: std.Address("g456"),
+ VotingPower: 10,
+ },
+ {
+ Address: std.Address("g789"),
+ VotingPower: 10,
+ },
+ }
+ }
+
+ // Create a proposal
+ title := "new govdao member addition"
+ description := "add new members to the govdao"
+
+ prop := dao.ProposalRequest{
+ Title: title,
+ Description: description,
+ Executor: govdao.NewMemberPropExecutor(memberFn),
+ }
+
+ bridge.GovDAO().Propose(prop)
+}
+
+func main() {
+ println("--")
+ println(govdao.GetMembStore().Size())
+ println("--")
+ println(govdao.Render(""))
+ println("--")
+ println(govdao.Render("0"))
+ println("--")
+ govdao.VoteOnProposal(0, "YES")
+ println("--")
+ println(govdao.Render("0"))
+ println("--")
+ println(govdao.Render(""))
+ println("--")
+ govdao.ExecuteProposal(0)
+ println("--")
+ println(govdao.Render("0"))
+ println("--")
+ println(govdao.Render(""))
+ println("--")
+ println(govdao.GetMembStore().Size())
+}
+
+// Output:
+// --
+// 1
+// --
+// # GovDAO Proposals
+//
+// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+//
+// --
+// # Proposal #0 - new govdao member addition
+//
+// ## Description
+//
+// add new members to the govdao
+//
+// ## Proposal information
+//
+// **Status: ACTIVE**
+//
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
+//
+//
+// **Threshold met: FALSE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
+//
+//
+// --
+// --
+// # Proposal #0 - new govdao member addition
+//
+// ## Description
+//
+// add new members to the govdao
+//
+// ## Proposal information
+//
+// **Status: ACCEPTED**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
+//
+//
+// --
+// # GovDAO Proposals
+//
+// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0)
+//
+// **Status: ACCEPTED**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+//
+// --
+// --
+// # Proposal #0 - new govdao member addition
+//
+// ## Description
+//
+// add new members to the govdao
+//
+// ## Proposal information
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Voting stats:**
+// - YES 10 (25%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 30 (75%)
+//
+//
+// **Threshold met: FALSE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
+//
+//
+// --
+// # GovDAO Proposals
+//
+// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0)
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+//
+// --
+// 4
+
+// Events:
+// [
+// {
+// "type": "ProposalAdded",
+// "attrs": [
+// {
+// "key": "proposal-id",
+// "value": "0"
+// },
+// {
+// "key": "proposal-author",
+// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"
+// }
+// ],
+// "pkg_path": "gno.land/r/gov/dao/v2",
+// "func": "EmitProposalAdded"
+// },
+// {
+// "type": "VoteAdded",
+// "attrs": [
+// {
+// "key": "proposal-id",
+// "value": "0"
+// },
+// {
+// "key": "author",
+// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"
+// },
+// {
+// "key": "option",
+// "value": "YES"
+// }
+// ],
+// "pkg_path": "gno.land/r/gov/dao/v2",
+// "func": "EmitVoteAdded"
+// },
+// {
+// "type": "ProposalAccepted",
+// "attrs": [
+// {
+// "key": "proposal-id",
+// "value": "0"
+// }
+// ],
+// "pkg_path": "gno.land/r/gov/dao/v2",
+// "func": "EmitProposalAccepted"
+// },
+// {
+// "type": "ProposalExecuted",
+// "attrs": [
+// {
+// "key": "proposal-id",
+// "value": "0"
+// },
+// {
+// "key": "exec-status",
+// "value": "accepted"
+// }
+// ],
+// "pkg_path": "gno.land/r/gov/dao/v2",
+// "func": "ExecuteProposal"
+// }
+// ]
diff --git a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno
new file mode 100644
index 00000000000..13ca572c512
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno
@@ -0,0 +1,132 @@
+package main
+
+import (
+ "gno.land/p/demo/dao"
+ "gno.land/r/gov/dao/bridge"
+ govdaov2 "gno.land/r/gov/dao/v2"
+ "gno.land/r/sys/params"
+)
+
+func init() {
+ mExec := params.NewStringPropExecutor("prop1.string", "value1")
+ title := "Setting prop1.string param"
+ comment := "setting prop1.string param"
+ prop := dao.ProposalRequest{
+ Title: title,
+ Description: comment,
+ Executor: mExec,
+ }
+ id := bridge.GovDAO().Propose(prop)
+ println("new prop", id)
+}
+
+func main() {
+ println("--")
+ println(govdaov2.Render(""))
+ println("--")
+ println(govdaov2.Render("0"))
+ println("--")
+ bridge.GovDAO().VoteOnProposal(0, "YES")
+ println("--")
+ println(govdaov2.Render("0"))
+ println("--")
+ bridge.GovDAO().ExecuteProposal(0)
+ println("--")
+ println(govdaov2.Render("0"))
+}
+
+// Output:
+// new prop 0
+// --
+// # GovDAO Proposals
+//
+// ## [Prop #0 - Setting prop1.string param](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+//
+// --
+// # Proposal #0 - Setting prop1.string param
+//
+// ## Description
+//
+// setting prop1.string param
+//
+// ## Proposal information
+//
+// **Status: ACTIVE**
+//
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
+//
+//
+// **Threshold met: FALSE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
+//
+//
+// --
+// --
+// # Proposal #0 - Setting prop1.string param
+//
+// ## Description
+//
+// setting prop1.string param
+//
+// ## Proposal information
+//
+// **Status: ACCEPTED**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
+//
+//
+// --
+// --
+// # Proposal #0 - Setting prop1.string param
+//
+// ## Description
+//
+// setting prop1.string param
+//
+// ## Proposal information
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
+//
+//
diff --git a/examples/gno.land/r/gov/dao/v2/render.gno b/examples/gno.land/r/gov/dao/v2/render.gno
new file mode 100644
index 00000000000..57b7b601523
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/v2/render.gno
@@ -0,0 +1,123 @@
+package govdao
+
+import (
+ "strconv"
+ "strings"
+
+ "gno.land/p/demo/dao"
+ "gno.land/p/demo/ufmt"
+ "gno.land/p/moul/txlink"
+ "gno.land/r/demo/users"
+)
+
+func Render(path string) string {
+ var out string
+
+ if path == "" {
+ out += "# GovDAO Proposals\n\n"
+ numProposals := d.Size()
+
+ if numProposals == 0 {
+ out += "No proposals found :(" // corner case
+ return out
+ }
+
+ offset := uint64(0)
+ if numProposals >= 10 {
+ offset = uint64(numProposals) - 10
+ }
+
+ // Fetch the last 10 proposals
+ proposals := d.Proposals(offset, uint64(10))
+ for i := len(proposals) - 1; i >= 0; i-- {
+ prop := proposals[i]
+
+ title := prop.Title()
+ if len(title) > 40 {
+ title = title[:40] + "..."
+ }
+
+ propID := offset + uint64(i)
+ out += ufmt.Sprintf("## [Prop #%d - %s](/r/gov/dao/v2:%d)\n\n", propID, title, propID)
+ out += ufmt.Sprintf("**Status: %s**\n\n", strings.ToUpper(prop.Status().String()))
+
+ user := users.GetUserByAddress(prop.Author())
+ authorDisplayText := prop.Author().String()
+ if user != nil {
+ authorDisplayText = ufmt.Sprintf("[%s](/r/demo/users:%s)", user.Name, user.Name)
+ }
+
+ out += ufmt.Sprintf("**Author: %s**\n\n", authorDisplayText)
+
+ if i != 0 {
+ out += "---\n\n"
+ }
+ }
+
+ return out
+ }
+
+ // Display the detailed proposal
+ idx, err := strconv.Atoi(path)
+ if err != nil {
+ return "404: Invalid proposal ID"
+ }
+
+ // Fetch the proposal
+ prop, err := d.ProposalByID(uint64(idx))
+ if err != nil {
+ return ufmt.Sprintf("unable to fetch proposal, %s", err.Error())
+ }
+
+ // Render the proposal page
+ out += renderPropPage(prop, idx)
+
+ return out
+}
+
+func renderPropPage(prop dao.Proposal, idx int) string {
+ var out string
+
+ out += ufmt.Sprintf("# Proposal #%d - %s\n\n", idx, prop.Title())
+ out += prop.Render()
+ out += renderAuthor(prop)
+ out += renderActionBar(prop, idx)
+ out += "\n\n"
+
+ return out
+}
+
+func renderAuthor(p dao.Proposal) string {
+ var out string
+
+ authorUsername := ""
+ user := users.GetUserByAddress(p.Author())
+ if user != nil {
+ authorUsername = user.Name
+ }
+
+ if authorUsername != "" {
+ out += ufmt.Sprintf("**Author: [%s](/r/demo/users:%s)**\n\n", authorUsername, authorUsername)
+ } else {
+ out += ufmt.Sprintf("**Author: %s**\n\n", p.Author().String())
+ }
+
+ return out
+}
+
+func renderActionBar(p dao.Proposal, idx int) string {
+ var out string
+
+ out += "### Actions\n\n"
+ if p.Status() == dao.Active {
+ out += ufmt.Sprintf("#### [[Vote YES](%s)] - [[Vote NO](%s)] - [[Vote ABSTAIN](%s)]",
+ txlink.Call("VoteOnProposal", "id", strconv.Itoa(idx), "option", "YES"),
+ txlink.Call("VoteOnProposal", "id", strconv.Itoa(idx), "option", "NO"),
+ txlink.Call("VoteOnProposal", "id", strconv.Itoa(idx), "option", "ABSTAIN"),
+ )
+ } else {
+ out += "The voting period for this proposal is over."
+ }
+
+ return out
+}
diff --git a/examples/gno.land/r/gov/dao/voter.gno b/examples/gno.land/r/gov/dao/voter.gno
deleted file mode 100644
index 99223210791..00000000000
--- a/examples/gno.land/r/gov/dao/voter.gno
+++ /dev/null
@@ -1,91 +0,0 @@
-package govdao
-
-import (
- "std"
-
- "gno.land/p/demo/ufmt"
-)
-
-const (
- yay = "YES"
- nay = "NO"
-
- msgNoMoreVotesAllowed = "no more votes allowed"
- msgAlreadyVoted = "caller already voted"
- msgWrongVotingValue = "voting values must be YES or NO"
-)
-
-func NewPercentageVoter(percent int) *PercentageVoter {
- if percent < 0 || percent > 100 {
- panic("percent value must be between 0 and 100")
- }
-
- return &PercentageVoter{
- percentage: percent,
- }
-}
-
-// PercentageVoter is a system based on the amount of received votes.
-// When the specified treshold is reached, the voting process finishes.
-type PercentageVoter struct {
- percentage int
-
- voters []std.Address
- yes int
- no int
-}
-
-func (pv *PercentageVoter) IsAccepted(voters []std.Address) bool {
- if len(voters) == 0 {
- return true // special case
- }
-
- return pv.percent(voters) >= pv.percentage
-}
-
-func (pv *PercentageVoter) IsFinished(voters []std.Address) bool {
- return pv.yes+pv.no >= len(voters)
-}
-
-func (pv *PercentageVoter) Status(voters []std.Address) string {
- return ufmt.Sprintf("YES: %d, NO: %d, percent: %d, members: %d", pv.yes, pv.no, pv.percent(voters), len(voters))
-}
-
-func (pv *PercentageVoter) Vote(voters []std.Address, caller std.Address, flag string) {
- if pv.IsFinished(voters) {
- panic(msgNoMoreVotesAllowed)
- }
-
- if pv.alreadyVoted(caller) {
- panic(msgAlreadyVoted)
- }
-
- switch flag {
- case yay:
- pv.yes++
- pv.voters = append(pv.voters, caller)
- case nay:
- pv.no++
- pv.voters = append(pv.voters, caller)
- default:
- panic(msgWrongVotingValue)
- }
-}
-
-func (pv *PercentageVoter) percent(voters []std.Address) int {
- if len(voters) == 0 {
- return 0
- }
-
- return int((float32(pv.yes) / float32(len(voters))) * 100)
-}
-
-func (pv *PercentageVoter) alreadyVoted(addr std.Address) bool {
- for _, v := range pv.voters {
- if v == addr {
- return true
- }
- }
-
- return false
-}
diff --git a/examples/gno.land/r/leon/config/config.gno b/examples/gno.land/r/leon/config/config.gno
new file mode 100644
index 00000000000..bc800ec8263
--- /dev/null
+++ b/examples/gno.land/r/leon/config/config.gno
@@ -0,0 +1,63 @@
+package config
+
+import (
+ "errors"
+ "std"
+)
+
+var (
+ main std.Address // leon's main address
+ backup std.Address // backup address
+
+ ErrInvalidAddr = errors.New("leon's config: invalid address")
+ ErrUnauthorized = errors.New("leon's config: unauthorized")
+)
+
+func init() {
+ main = "g125em6arxsnj49vx35f0n0z34putv5ty3376fg5"
+}
+
+func Address() std.Address {
+ return main
+}
+
+func Backup() std.Address {
+ return backup
+}
+
+func SetAddress(a std.Address) error {
+ if !a.IsValid() {
+ return ErrInvalidAddr
+ }
+
+ if err := checkAuthorized(); err != nil {
+ return err
+ }
+
+ main = a
+ return nil
+}
+
+func SetBackup(a std.Address) error {
+ if !a.IsValid() {
+ return ErrInvalidAddr
+ }
+
+ if err := checkAuthorized(); err != nil {
+ return err
+ }
+
+ backup = a
+ return nil
+}
+
+func checkAuthorized() error {
+ caller := std.PrevRealm().Addr()
+ isAuthorized := caller == main || caller == backup
+
+ if !isAuthorized {
+ return ErrUnauthorized
+ }
+
+ return nil
+}
diff --git a/examples/gno.land/r/leon/config/gno.mod b/examples/gno.land/r/leon/config/gno.mod
new file mode 100644
index 00000000000..e8cd5cd85b7
--- /dev/null
+++ b/examples/gno.land/r/leon/config/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/leon/config
diff --git a/examples/gno.land/r/leon/hof/datasource.gno b/examples/gno.land/r/leon/hof/datasource.gno
new file mode 100644
index 00000000000..180c4880177
--- /dev/null
+++ b/examples/gno.land/r/leon/hof/datasource.gno
@@ -0,0 +1,77 @@
+package hof
+
+import (
+ "errors"
+
+ "gno.land/p/demo/avl"
+ "gno.land/p/demo/ufmt"
+ "gno.land/p/jeronimoalbi/datasource"
+)
+
+func NewDatasource() Datasource {
+ return Datasource{exhibition}
+}
+
+type Datasource struct {
+ exhibition *Exhibition
+}
+
+func (ds Datasource) Size() int { return ds.exhibition.itemsSorted.Size() }
+
+func (ds Datasource) Records(q datasource.Query) datasource.Iterator {
+ return &iterator{
+ exhibition: ds.exhibition,
+ index: q.Offset,
+ maxIndex: q.Offset + q.Count,
+ }
+}
+
+func (ds Datasource) Record(id string) (datasource.Record, error) {
+ v, found := ds.exhibition.itemsSorted.Get(id)
+ if !found {
+ return nil, errors.New("realm submission not found")
+ }
+ return record{v.(*Item)}, nil
+}
+
+type record struct {
+ item *Item
+}
+
+func (r record) ID() string { return r.item.id.String() }
+func (r record) String() string { return r.item.pkgpath }
+
+func (r record) Fields() (datasource.Fields, error) {
+ fields := avl.NewTree()
+ fields.Set(
+ "details",
+ ufmt.Sprintf("Votes: ⏶ %d - ⏷ %d", r.item.upvote.Size(), r.item.downvote.Size()),
+ )
+ return fields, nil
+}
+
+func (r record) Content() (string, error) {
+ content := ufmt.Sprintf("# Submission #%d\n\n", int(r.item.id))
+ content += r.item.Render(false)
+ return content, nil
+}
+
+type iterator struct {
+ exhibition *Exhibition
+ index, maxIndex int
+ record *record
+}
+
+func (it iterator) Record() datasource.Record { return it.record }
+func (it iterator) Err() error { return nil }
+
+func (it *iterator) Next() bool {
+ if it.index >= it.maxIndex || it.index >= it.exhibition.itemsSorted.Size() {
+ return false
+ }
+
+ _, v := it.exhibition.itemsSorted.GetByIndex(it.index)
+ it.record = &record{v.(*Item)}
+ it.index++
+ return true
+}
diff --git a/examples/gno.land/r/leon/hof/datasource_test.gno b/examples/gno.land/r/leon/hof/datasource_test.gno
new file mode 100644
index 00000000000..376f981875f
--- /dev/null
+++ b/examples/gno.land/r/leon/hof/datasource_test.gno
@@ -0,0 +1,157 @@
+package hof
+
+import (
+ "testing"
+
+ "gno.land/p/demo/avl"
+ "gno.land/p/demo/uassert"
+ "gno.land/p/demo/urequire"
+ "gno.land/p/jeronimoalbi/datasource"
+)
+
+var (
+ _ datasource.Datasource = (*Datasource)(nil)
+ _ datasource.Record = (*record)(nil)
+ _ datasource.ContentRecord = (*record)(nil)
+ _ datasource.Iterator = (*iterator)(nil)
+)
+
+func TestDatasourceRecords(t *testing.T) {
+ cases := []struct {
+ name string
+ items []*Item
+ recordIDs []string
+ options []datasource.QueryOption
+ }{
+ {
+ name: "all items",
+ items: []*Item{{id: 1}, {id: 2}, {id: 3}},
+ recordIDs: []string{"0000001", "0000002", "0000003"},
+ },
+ {
+ name: "with offset",
+ items: []*Item{{id: 1}, {id: 2}, {id: 3}},
+ recordIDs: []string{"0000002", "0000003"},
+ options: []datasource.QueryOption{datasource.WithOffset(1)},
+ },
+ {
+ name: "with count",
+ items: []*Item{{id: 1}, {id: 2}, {id: 3}},
+ recordIDs: []string{"0000001", "0000002"},
+ options: []datasource.QueryOption{datasource.WithCount(2)},
+ },
+ {
+ name: "with offset and count",
+ items: []*Item{{id: 1}, {id: 2}, {id: 3}},
+ recordIDs: []string{"0000002"},
+ options: []datasource.QueryOption{
+ datasource.WithOffset(1),
+ datasource.WithCount(1),
+ },
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Initialize a local instance of exhibition
+ exhibition := &Exhibition{itemsSorted: avl.NewTree()}
+ for _, item := range tc.items {
+ exhibition.itemsSorted.Set(item.id.String(), item)
+ }
+
+ // Get a records iterator
+ ds := Datasource{exhibition}
+ query := datasource.NewQuery(tc.options...)
+ iter := ds.Records(query)
+
+ // Start asserting
+ urequire.Equal(t, len(tc.items), ds.Size(), "datasource size")
+
+ var records []datasource.Record
+ for iter.Next() {
+ records = append(records, iter.Record())
+ }
+ urequire.Equal(t, len(tc.recordIDs), len(records), "record count")
+
+ for i, r := range records {
+ uassert.Equal(t, tc.recordIDs[i], r.ID())
+ }
+ })
+ }
+}
+
+func TestDatasourceRecord(t *testing.T) {
+ cases := []struct {
+ name string
+ items []*Item
+ id string
+ err string
+ }{
+ {
+ name: "found",
+ items: []*Item{{id: 1}, {id: 2}, {id: 3}},
+ id: "0000001",
+ },
+ {
+ name: "no found",
+ items: []*Item{{id: 1}, {id: 2}, {id: 3}},
+ id: "42",
+ err: "realm submission not found",
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Initialize a local instance of exhibition
+ exhibition := &Exhibition{itemsSorted: avl.NewTree()}
+ for _, item := range tc.items {
+ exhibition.itemsSorted.Set(item.id.String(), item)
+ }
+
+ // Get a single record
+ ds := Datasource{exhibition}
+ r, err := ds.Record(tc.id)
+
+ // Start asserting
+ if tc.err != "" {
+ uassert.ErrorContains(t, err, tc.err)
+ return
+ }
+
+ urequire.NoError(t, err, "no error")
+ urequire.NotEqual(t, nil, r, "record not nil")
+ uassert.Equal(t, tc.id, r.ID())
+ })
+ }
+}
+
+func TestItemRecord(t *testing.T) {
+ pkgpath := "gno.land/r/demo/test"
+ item := Item{
+ id: 1,
+ pkgpath: pkgpath,
+ blockNum: 42,
+ upvote: avl.NewTree(),
+ downvote: avl.NewTree(),
+ }
+ item.downvote.Set("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", struct{}{})
+ item.upvote.Set("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", struct{}{})
+ item.upvote.Set("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", struct{}{})
+
+ r := record{&item}
+
+ uassert.Equal(t, "0000001", r.ID())
+ uassert.Equal(t, pkgpath, r.String())
+
+ fields, _ := r.Fields()
+ details, found := fields.Get("details")
+ urequire.True(t, found, "details field")
+ uassert.Equal(t, "Votes: ⏶ 2 - ⏷ 1", details)
+
+ content, _ := r.Content()
+ wantContent := "# Submission #1\n\n\n```\ngno.land/r/demo/test\n```\n\nby demo\n\n" +
+ "[View realm](/r/demo/test)\n\nSubmitted at Block #42\n\n" +
+ "#### [2👍](/r/leon/hof$help&func=Upvote&pkgpath=gno.land/r/demo/test) - " +
+ "[1👎](/r/leon/hof$help&func=Downvote&pkgpath=gno.land/r/demo/test)\n\n"
+ uassert.Equal(t, wantContent, content)
+}
diff --git a/examples/gno.land/r/leon/hof/errors.gno b/examples/gno.land/r/leon/hof/errors.gno
new file mode 100644
index 00000000000..7277f65fa76
--- /dev/null
+++ b/examples/gno.land/r/leon/hof/errors.gno
@@ -0,0 +1,11 @@
+package hof
+
+import (
+ "errors"
+)
+
+var (
+ ErrNoSuchItem = errors.New("hof: no such item exists")
+ ErrDoubleUpvote = errors.New("hof: cannot upvote twice")
+ ErrDoubleDownvote = errors.New("hof: cannot downvote twice")
+)
diff --git a/examples/gno.land/r/leon/hof/gno.mod b/examples/gno.land/r/leon/hof/gno.mod
new file mode 100644
index 00000000000..f4720eb2b5a
--- /dev/null
+++ b/examples/gno.land/r/leon/hof/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/leon/hof
diff --git a/examples/gno.land/r/leon/hof/hof.gno b/examples/gno.land/r/leon/hof/hof.gno
new file mode 100644
index 00000000000..147a0dd1a95
--- /dev/null
+++ b/examples/gno.land/r/leon/hof/hof.gno
@@ -0,0 +1,134 @@
+// Package hof is the hall of fame realm.
+// The Hall of Fame is an exhibition that holds items. Users can add their realms to the Hall of Fame by
+// importing the Hall of Fame realm and calling hof.Register() from their init function.
+package hof
+
+import (
+ "std"
+
+ "gno.land/p/demo/avl"
+ "gno.land/p/demo/ownable"
+ "gno.land/p/demo/pausable"
+ "gno.land/p/demo/seqid"
+)
+
+var (
+ exhibition *Exhibition
+
+ // Safe objects
+ Ownable *ownable.Ownable
+ Pausable *pausable.Pausable
+)
+
+type (
+ Exhibition struct {
+ itemCounter seqid.ID
+ description string
+ items *avl.Tree // pkgPath > Item
+ itemsSorted *avl.Tree // same data but sorted, storing pointers
+ }
+
+ Item struct {
+ id seqid.ID
+ pkgpath string
+ blockNum int64
+ upvote *avl.Tree // std.Addr > struct{}{}
+ downvote *avl.Tree // std.Addr > struct{}{}
+ }
+)
+
+func init() {
+ exhibition = &Exhibition{
+ items: avl.NewTree(),
+ itemsSorted: avl.NewTree(),
+ }
+
+ Ownable = ownable.NewWithAddress(std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5"))
+ Pausable = pausable.NewFromOwnable(Ownable)
+}
+
+// Register registers your realm to the Hall of Fame
+// Should be called from within code
+func Register() {
+ if Pausable.IsPaused() {
+ return
+ }
+
+ submission := std.PrevRealm()
+ pkgpath := submission.PkgPath()
+
+ // Must be called from code
+ if submission.IsUser() {
+ return
+ }
+
+ // Must not yet exist
+ if exhibition.items.Has(pkgpath) {
+ return
+ }
+
+ id := exhibition.itemCounter.Next()
+ i := &Item{
+ id: id,
+ pkgpath: pkgpath,
+ blockNum: std.GetHeight(),
+ upvote: avl.NewTree(),
+ downvote: avl.NewTree(),
+ }
+
+ exhibition.items.Set(pkgpath, i)
+ exhibition.itemsSorted.Set(id.String(), i)
+
+ std.Emit("Registration")
+}
+
+func Upvote(pkgpath string) {
+ rawItem, ok := exhibition.items.Get(pkgpath)
+ if !ok {
+ panic(ErrNoSuchItem.Error())
+ }
+
+ item := rawItem.(*Item)
+ caller := std.PrevRealm().Addr().String()
+
+ if item.upvote.Has(caller) {
+ panic(ErrDoubleUpvote.Error())
+ }
+
+ item.upvote.Set(caller, struct{}{})
+}
+
+func Downvote(pkgpath string) {
+ rawItem, ok := exhibition.items.Get(pkgpath)
+ if !ok {
+ panic(ErrNoSuchItem.Error())
+ }
+
+ item := rawItem.(*Item)
+ caller := std.PrevRealm().Addr().String()
+
+ if item.downvote.Has(caller) {
+ panic(ErrDoubleDownvote.Error())
+ }
+
+ item.downvote.Set(caller, struct{}{})
+}
+
+func Delete(pkgpath string) {
+ if !Ownable.CallerIsOwner() {
+ panic(ownable.ErrUnauthorized.Error())
+ }
+
+ i, ok := exhibition.items.Get(pkgpath)
+ if !ok {
+ panic(ErrNoSuchItem.Error())
+ }
+
+ if _, removed := exhibition.itemsSorted.Remove(i.(*Item).id.String()); !removed {
+ panic(ErrNoSuchItem.Error())
+ }
+
+ if _, removed := exhibition.items.Remove(pkgpath); !removed {
+ panic(ErrNoSuchItem.Error())
+ }
+}
diff --git a/examples/gno.land/r/leon/hof/hof_test.gno b/examples/gno.land/r/leon/hof/hof_test.gno
new file mode 100644
index 00000000000..4d6f70eab88
--- /dev/null
+++ b/examples/gno.land/r/leon/hof/hof_test.gno
@@ -0,0 +1,134 @@
+package hof
+
+import (
+ "std"
+ "testing"
+
+ "gno.land/p/demo/testutils"
+ "gno.land/p/demo/uassert"
+ "gno.land/p/demo/urequire"
+)
+
+const rlmPath = "gno.land/r/gnoland/home"
+
+var (
+ admin = Ownable.Owner()
+ adminRealm = std.NewUserRealm(admin)
+ alice = testutils.TestAddress("alice")
+)
+
+func TestRegister(t *testing.T) {
+ // Test user realm register
+ aliceRealm := std.NewUserRealm(alice)
+ std.TestSetRealm(aliceRealm)
+
+ Register()
+ uassert.False(t, itemExists(t, rlmPath))
+
+ // Test register while paused
+ std.TestSetRealm(adminRealm)
+ Pausable.Pause()
+
+ // Set legitimate caller
+ std.TestSetRealm(std.NewCodeRealm(rlmPath))
+
+ Register()
+ uassert.False(t, itemExists(t, rlmPath))
+
+ // Unpause
+ std.TestSetRealm(adminRealm)
+ Pausable.Unpause()
+
+ // Set legitimate caller
+ std.TestSetRealm(std.NewCodeRealm(rlmPath))
+ Register()
+
+ // Find registered items
+ uassert.True(t, itemExists(t, rlmPath))
+}
+
+func TestUpvote(t *testing.T) {
+ raw, _ := exhibition.items.Get(rlmPath)
+ item := raw.(*Item)
+
+ rawSorted, _ := exhibition.itemsSorted.Get(item.id.String())
+ itemSorted := rawSorted.(*Item)
+
+ // 0 upvotes by default
+ urequire.Equal(t, item.upvote.Size(), 0)
+
+ std.TestSetRealm(adminRealm)
+
+ urequire.NotPanics(t, func() {
+ Upvote(rlmPath)
+ })
+
+ // Check both trees for 1 upvote
+ uassert.Equal(t, item.upvote.Size(), 1)
+ uassert.Equal(t, itemSorted.upvote.Size(), 1)
+
+ // Check double upvote
+ uassert.PanicsWithMessage(t, ErrDoubleUpvote.Error(), func() {
+ Upvote(rlmPath)
+ })
+}
+
+func TestDownvote(t *testing.T) {
+ raw, _ := exhibition.items.Get(rlmPath)
+ item := raw.(*Item)
+
+ rawSorted, _ := exhibition.itemsSorted.Get(item.id.String())
+ itemSorted := rawSorted.(*Item)
+
+ // 0 downvotes by default
+ urequire.Equal(t, item.downvote.Size(), 0)
+
+ userRealm := std.NewUserRealm(alice)
+ std.TestSetRealm(userRealm)
+
+ urequire.NotPanics(t, func() {
+ Downvote(rlmPath)
+ })
+
+ // Check both trees for 1 upvote
+ uassert.Equal(t, item.downvote.Size(), 1)
+ uassert.Equal(t, itemSorted.downvote.Size(), 1)
+
+ // Check double downvote
+ uassert.PanicsWithMessage(t, ErrDoubleDownvote.Error(), func() {
+ Downvote(rlmPath)
+ })
+}
+
+func TestDelete(t *testing.T) {
+ userRealm := std.NewUserRealm(admin)
+ std.TestSetRealm(userRealm)
+ std.TestSetOrigCaller(admin)
+
+ uassert.PanicsWithMessage(t, ErrNoSuchItem.Error(), func() {
+ Delete("nonexistentpkgpath")
+ })
+
+ i, _ := exhibition.items.Get(rlmPath)
+ id := i.(*Item).id
+
+ uassert.NotPanics(t, func() {
+ Delete(rlmPath)
+ })
+
+ uassert.False(t, exhibition.items.Has(rlmPath))
+ uassert.False(t, exhibition.itemsSorted.Has(id.String()))
+}
+
+func itemExists(t *testing.T, rlmPath string) bool {
+ t.Helper()
+
+ i, ok1 := exhibition.items.Get(rlmPath)
+ ok2 := false
+
+ if ok1 {
+ _, ok2 = exhibition.itemsSorted.Get(i.(*Item).id.String())
+ }
+
+ return ok1 && ok2
+}
diff --git a/examples/gno.land/r/leon/hof/render.gno b/examples/gno.land/r/leon/hof/render.gno
new file mode 100644
index 00000000000..868262bedc7
--- /dev/null
+++ b/examples/gno.land/r/leon/hof/render.gno
@@ -0,0 +1,113 @@
+package hof
+
+import (
+ "strings"
+
+ "gno.land/p/demo/avl/pager"
+ "gno.land/p/demo/fqname"
+ "gno.land/p/demo/seqid"
+ "gno.land/p/demo/ufmt"
+ "gno.land/p/moul/txlink"
+)
+
+const (
+ pageSize = 5
+)
+
+func Render(path string) string {
+ out := "# Hall of Fame\n\n"
+
+ dashboardEnabled := path == "dashboard"
+
+ if dashboardEnabled {
+ out += renderDashboard()
+ }
+
+ out += exhibition.Render(path, dashboardEnabled)
+
+ return out
+}
+
+func (e Exhibition) Render(path string, dashboard bool) string {
+ out := ufmt.Sprintf("%s\n\n", e.description)
+
+ if e.items.Size() == 0 {
+ out += "No items in this exhibition currently.\n\n"
+ return out
+ }
+
+ out += "
\n\n"
+
+ page := pager.NewPager(e.itemsSorted, pageSize, false).MustGetPageByPath(path)
+
+ for i := len(page.Items) - 1; i >= 0; i-- {
+ item := page.Items[i]
+
+ out += "
\n\n"
+ id, _ := seqid.FromString(item.Key)
+ out += ufmt.Sprintf("### Submission #%d\n\n", int(id))
+ out += item.Value.(*Item).Render(dashboard)
+ out += "
"
+ }
+
+ out += "
\n\n"
+
+ out += page.Picker()
+
+ return out
+}
+
+func (i Item) Render(dashboard bool) string {
+ out := ufmt.Sprintf("\n```\n%s\n```\n\n", i.pkgpath)
+ out += ufmt.Sprintf("by %s\n\n", strings.Split(i.pkgpath, "/")[2])
+ out += ufmt.Sprintf("[View realm](%s)\n\n", strings.TrimPrefix(i.pkgpath, "gno.land")) // gno.land/r/leon/home > /r/leon/home
+ out += ufmt.Sprintf("Submitted at Block #%d\n\n", i.blockNum)
+
+ out += ufmt.Sprintf("#### [%d👍](%s) - [%d👎](%s)\n\n",
+ i.upvote.Size(), txlink.Call("Upvote", "pkgpath", i.pkgpath),
+ i.downvote.Size(), txlink.Call("Downvote", "pkgpath", i.pkgpath),
+ )
+
+ if dashboard {
+ out += ufmt.Sprintf("[Delete](%s)", txlink.Call("Delete", "pkgpath", i.pkgpath))
+ }
+
+ return out
+}
+
+func renderDashboard() string {
+ out := "---\n\n"
+ out += "## Dashboard\n\n"
+ out += ufmt.Sprintf("Total submissions: %d\n\n", exhibition.items.Size())
+
+ out += ufmt.Sprintf("Exhibition admin: %s\n\n", Ownable.Owner().String())
+
+ if !Pausable.IsPaused() {
+ out += ufmt.Sprintf("[Pause exhibition](%s)\n\n", txlink.Call("Pause"))
+ } else {
+ out += ufmt.Sprintf("[Unpause exhibition](%s)\n\n", txlink.Call("Unpause"))
+ }
+
+ out += "---\n\n"
+
+ return out
+}
+
+func RenderExhibWidget(itemsToRender int) string {
+ if itemsToRender < 1 {
+ return ""
+ }
+
+ out := ""
+ i := 0
+ exhibition.items.Iterate("", "", func(key string, value interface{}) bool {
+ item := value.(*Item)
+
+ out += ufmt.Sprintf("- %s\n", fqname.RenderLink(item.pkgpath, ""))
+
+ i++
+ return i >= itemsToRender
+ })
+
+ return out
+}
diff --git a/examples/gno.land/r/leon/home/gno.mod b/examples/gno.land/r/leon/home/gno.mod
new file mode 100644
index 00000000000..56fea265e29
--- /dev/null
+++ b/examples/gno.land/r/leon/home/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/leon/home
diff --git a/examples/gno.land/r/leon/home/home.gno b/examples/gno.land/r/leon/home/home.gno
new file mode 100644
index 00000000000..cf33260cc6b
--- /dev/null
+++ b/examples/gno.land/r/leon/home/home.gno
@@ -0,0 +1,136 @@
+package home
+
+import (
+ "std"
+ "strconv"
+
+ "gno.land/p/demo/ufmt"
+
+ "gno.land/r/demo/art/gnoface"
+ "gno.land/r/demo/art/millipede"
+ "gno.land/r/demo/mirror"
+ "gno.land/r/leon/config"
+ "gno.land/r/leon/hof"
+)
+
+var (
+ pfp string // link to profile picture
+ pfpCaption string // profile picture caption
+ abtMe [2]string
+)
+
+func init() {
+ pfp = "https://i.imgflip.com/91vskx.jpg"
+ pfpCaption = "[My favourite painting & pfp](https://en.wikipedia.org/wiki/Wanderer_above_the_Sea_of_Fog)"
+ abtMe = [2]string{
+ `### About me
+Hi, I'm Leon, a DevRel Engineer at gno.land. I am a tech enthusiast,
+life-long learner, and sharer of knowledge.`,
+ `### Contributions
+My contributions to gno.land can mainly be found
+[here](https://github.com/gnolang/gno/issues?q=sort:updated-desc+author:leohhhn).
+
+TODO import r/gh
+`,
+ }
+
+ hof.Register()
+ mirror.Register(std.CurrentRealm().PkgPath(), Render)
+}
+
+func UpdatePFP(url, caption string) {
+ if !isAuthorized(std.PrevRealm().Addr()) {
+ panic(config.ErrUnauthorized)
+ }
+
+ pfp = url
+ pfpCaption = caption
+}
+
+func UpdateAboutMe(col1, col2 string) {
+ if !isAuthorized(std.PrevRealm().Addr()) {
+ panic(config.ErrUnauthorized)
+ }
+
+ abtMe[0] = col1
+ abtMe[1] = col2
+}
+
+func Render(path string) string {
+ out := "# Leon's Homepage\n\n"
+
+ out += renderAboutMe()
+ out += renderBlogPosts()
+ out += "\n\n"
+ out += renderArt()
+
+ return out
+}
+
+func renderBlogPosts() string {
+ out := ""
+ //out += "## Leon's Blog Posts"
+
+ // todo fetch blog posts authored by @leohhhn
+ // and render them
+ return out
+}
+
+func renderAboutMe() string {
+ out := "
"
+
+ out += "
\n\n"
+ out += ufmt.Sprintf("![my profile pic](%s)\n\n%s\n\n", pfp, pfpCaption)
+ out += "
\n\n"
+
+ out += "
\n\n"
+ out += abtMe[0] + "\n\n"
+ out += "
\n\n"
+
+ out += "
\n\n"
+ out += abtMe[1] + "\n\n"
+ out += "
\n\n"
+
+ out += "
\n\n"
+
+ return out
+}
+
+func renderArt() string {
+ out := `
` + "\n\n"
+ out += "# Gno Art\n\n"
+
+ out += "
"
+
+ out += renderGnoFace()
+ out += renderMillipede()
+ out += "Empty spot :/"
+
+ out += "
\n\n"
+
+ out += "This art is dynamic; it will change with every new block.\n\n"
+ out += `
` + "\n"
+
+ return out
+}
+
+func renderGnoFace() string {
+ out := "
\n\n"
+ out += gnoface.Render(strconv.Itoa(int(std.GetHeight())))
+ out += "
\n\n"
+
+ return out
+}
+
+func renderMillipede() string {
+ out := "
\n\n"
+ out += "Millipede\n\n"
+ out += "```\n" + millipede.Draw(int(std.GetHeight())%10+1) + "```\n"
+ out += "
\n\n"
+
+ return out
+}
+
+func isAuthorized(addr std.Address) bool {
+ return addr == config.Address() || addr == config.Backup()
+}
diff --git a/examples/gno.land/r/manfred/config/config.gno b/examples/gno.land/r/manfred/config/config.gno
deleted file mode 100644
index 23e90df50ff..00000000000
--- a/examples/gno.land/r/manfred/config/config.gno
+++ /dev/null
@@ -1,20 +0,0 @@
-package config
-
-import "std"
-
-var addr = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq")
-
-func Addr() std.Address {
- return addr
-}
-
-func UpdateAddr(newAddr std.Address) {
- AssertIsAdmin()
- addr = newAddr
-}
-
-func AssertIsAdmin() {
- if std.GetOrigCaller() != addr {
- panic("restricted area")
- }
-}
diff --git a/examples/gno.land/r/manfred/config/gno.mod b/examples/gno.land/r/manfred/config/gno.mod
deleted file mode 100644
index 516bf38528e..00000000000
--- a/examples/gno.land/r/manfred/config/gno.mod
+++ /dev/null
@@ -1 +0,0 @@
-module gno.land/r/manfred/config
diff --git a/examples/gno.land/r/manfred/home/gno.mod b/examples/gno.land/r/manfred/home/gno.mod
index 6e7aac70cc7..2efefe1824f 100644
--- a/examples/gno.land/r/manfred/home/gno.mod
+++ b/examples/gno.land/r/manfred/home/gno.mod
@@ -1,3 +1 @@
module gno.land/r/manfred/home
-
-require gno.land/r/manfred/config v0.0.0-latest
diff --git a/examples/gno.land/r/manfred/home/home.gno b/examples/gno.land/r/manfred/home/home.gno
old mode 100644
new mode 100755
index 720796a2201..56caf30d9fd
--- a/examples/gno.land/r/manfred/home/home.gno
+++ b/examples/gno.land/r/manfred/home/home.gno
@@ -1,56 +1,5 @@
package home
-import "gno.land/r/manfred/config"
-
-var (
- todos []string
- status string
- memeImgURL string
-)
-
-func init() {
- todos = append(todos, "fill this todo list...")
- status = "Online" // Initial status set to "Online"
- memeImgURL = "https://i.imgflip.com/7ze8dc.jpg"
-}
-
func Render(path string) string {
- content := "# Manfred's (gn)home Dashboard\n\n"
-
- content += "## Meme\n"
- content += "![](" + memeImgURL + ")\n\n"
-
- content += "## Status\n"
- content += status + "\n\n"
-
- content += "## Personal ToDo List\n"
- for _, todo := range todos {
- content += "- [ ] " + todo + "\n"
- }
- content += "\n"
-
- // TODO: Implement a feature to list replies on r/boards on my posts
- // TODO: Maybe integrate a calendar feature for upcoming events?
-
- return content
-}
-
-func AddNewTodo(todo string) {
- config.AssertIsAdmin()
- todos = append(todos, todo)
-}
-
-func DeleteTodo(todoIndex int) {
- config.AssertIsAdmin()
- if todoIndex >= 0 && todoIndex < len(todos) {
- // Remove the todo from the list by merging slices from before and after the todo
- todos = append(todos[:todoIndex], todos[todoIndex+1:]...)
- } else {
- panic("Invalid todo index")
- }
-}
-
-func UpdateStatus(newStatus string) {
- config.AssertIsAdmin()
- status = newStatus
+ return "Moved to r/moul"
}
diff --git a/examples/gno.land/r/manfred/home/z1_filetest.gno b/examples/gno.land/r/manfred/home/z1_filetest.gno
deleted file mode 100644
index 801efedb306..00000000000
--- a/examples/gno.land/r/manfred/home/z1_filetest.gno
+++ /dev/null
@@ -1,19 +0,0 @@
-package main
-
-import "gno.land/r/manfred/home"
-
-func main() {
- println(home.Render(""))
-}
-
-// Output:
-// # Manfred's (gn)home Dashboard
-//
-// ## Meme
-// ![](https://i.imgflip.com/7ze8dc.jpg)
-//
-// ## Status
-// Online
-//
-// ## Personal ToDo List
-// - [ ] fill this todo list...
diff --git a/examples/gno.land/r/manfred/home/z2_filetest.gno b/examples/gno.land/r/manfred/home/z2_filetest.gno
deleted file mode 100644
index 316fd400867..00000000000
--- a/examples/gno.land/r/manfred/home/z2_filetest.gno
+++ /dev/null
@@ -1,35 +0,0 @@
-package main
-
-import (
- "std"
-
- "gno.land/r/manfred/home"
-)
-
-func main() {
- std.TestSetOrigCaller("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq")
- home.AddNewTodo("aaa")
- home.AddNewTodo("bbb")
- home.AddNewTodo("ccc")
- home.AddNewTodo("ddd")
- home.AddNewTodo("eee")
- home.UpdateStatus("Lorem Ipsum")
- home.DeleteTodo(3)
- println(home.Render(""))
-}
-
-// Output:
-// # Manfred's (gn)home Dashboard
-//
-// ## Meme
-// ![](https://i.imgflip.com/7ze8dc.jpg)
-//
-// ## Status
-// Lorem Ipsum
-//
-// ## Personal ToDo List
-// - [ ] fill this todo list...
-// - [ ] aaa
-// - [ ] bbb
-// - [ ] ddd
-// - [ ] eee
diff --git a/examples/gno.land/r/manfred/present/admin.gno b/examples/gno.land/r/manfred/present/admin.gno
deleted file mode 100644
index 60af578b54f..00000000000
--- a/examples/gno.land/r/manfred/present/admin.gno
+++ /dev/null
@@ -1,96 +0,0 @@
-package present
-
-import (
- "std"
- "strings"
-
- "gno.land/p/demo/avl"
-)
-
-var (
- adminAddr std.Address
- moderatorList avl.Tree
- inPause bool
-)
-
-func init() {
- // adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis.
- adminAddr = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq"
-}
-
-func AdminSetAdminAddr(addr std.Address) {
- assertIsAdmin()
- adminAddr = addr
-}
-
-func AdminSetInPause(state bool) {
- assertIsAdmin()
- inPause = state
-}
-
-func AdminAddModerator(addr std.Address) {
- assertIsAdmin()
- moderatorList.Set(addr.String(), true)
-}
-
-func AdminRemoveModerator(addr std.Address) {
- assertIsAdmin()
- moderatorList.Set(addr.String(), false) // XXX: delete instead?
-}
-
-func ModAddPost(slug, title, body, publicationDate, authors, tags string) {
- assertIsModerator()
-
- caller := std.GetOrigCaller()
- tagList := strings.Split(tags, ",")
- authorList := strings.Split(authors, ",")
-
- err := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList)
- checkErr(err)
-}
-
-func ModEditPost(slug, title, body, publicationDate, authors, tags string) {
- assertIsModerator()
-
- tagList := strings.Split(tags, ",")
- authorList := strings.Split(authors, ",")
-
- err := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList)
- checkErr(err)
-}
-
-func isAdmin(addr std.Address) bool {
- return addr == adminAddr
-}
-
-func isModerator(addr std.Address) bool {
- _, found := moderatorList.Get(addr.String())
- return found
-}
-
-func assertIsAdmin() {
- caller := std.GetOrigCaller()
- if !isAdmin(caller) {
- panic("access restricted.")
- }
-}
-
-func assertIsModerator() {
- caller := std.GetOrigCaller()
- if isAdmin(caller) || isModerator(caller) {
- return
- }
- panic("access restricted")
-}
-
-func assertNotInPause() {
- if inPause {
- panic("access restricted (pause)")
- }
-}
-
-func checkErr(err error) {
- if err != nil {
- panic(err)
- }
-}
diff --git a/examples/gno.land/r/manfred/present/gno.mod b/examples/gno.land/r/manfred/present/gno.mod
deleted file mode 100644
index 5d50447e0e0..00000000000
--- a/examples/gno.land/r/manfred/present/gno.mod
+++ /dev/null
@@ -1,6 +0,0 @@
-module gno.land/r/manfred/present
-
-require (
- gno.land/p/demo/avl v0.0.0-latest
- gno.land/p/demo/blog v0.0.0-latest
-)
diff --git a/examples/gno.land/r/matijamarjanovic/home/config.gno b/examples/gno.land/r/matijamarjanovic/home/config.gno
new file mode 100644
index 00000000000..2a9669c0b58
--- /dev/null
+++ b/examples/gno.land/r/matijamarjanovic/home/config.gno
@@ -0,0 +1,64 @@
+package home
+
+import (
+ "errors"
+ "std"
+)
+
+var (
+ mainAddr = std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y") // matija's main address
+ backupAddr std.Address // backup address
+
+ errorInvalidAddr = errors.New("config: invalid address")
+ errorUnauthorized = errors.New("config: unauthorized")
+)
+
+func Address() std.Address {
+ return mainAddr
+}
+
+func Backup() std.Address {
+ return backupAddr
+}
+
+func SetAddress(newAddress std.Address) error {
+ if !newAddress.IsValid() {
+ return errorInvalidAddr
+ }
+
+ if err := checkAuthorized(); err != nil {
+ return err
+ }
+
+ mainAddr = newAddress
+ return nil
+}
+
+func SetBackup(newAddress std.Address) error {
+ if !newAddress.IsValid() {
+ return errorInvalidAddr
+ }
+
+ if err := checkAuthorized(); err != nil {
+ return err
+ }
+
+ backupAddr = newAddress
+ return nil
+}
+
+func checkAuthorized() error {
+ caller := std.GetOrigCaller()
+ if caller != mainAddr && caller != backupAddr {
+ return errorUnauthorized
+ }
+
+ return nil
+}
+
+func AssertAuthorized() {
+ caller := std.GetOrigCaller()
+ if caller != mainAddr && caller != backupAddr {
+ panic(errorUnauthorized)
+ }
+}
diff --git a/examples/gno.land/r/matijamarjanovic/home/gno.mod b/examples/gno.land/r/matijamarjanovic/home/gno.mod
new file mode 100644
index 00000000000..0457c947c01
--- /dev/null
+++ b/examples/gno.land/r/matijamarjanovic/home/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/matijamarjanovic/home
diff --git a/examples/gno.land/r/matijamarjanovic/home/home.gno b/examples/gno.land/r/matijamarjanovic/home/home.gno
new file mode 100644
index 00000000000..3757324108a
--- /dev/null
+++ b/examples/gno.land/r/matijamarjanovic/home/home.gno
@@ -0,0 +1,238 @@
+package home
+
+import (
+ "std"
+ "strings"
+
+ "gno.land/p/demo/ufmt"
+ "gno.land/p/moul/md"
+ "gno.land/r/leon/hof"
+)
+
+var (
+ pfp string // link to profile picture
+ pfpCaption string // profile picture caption
+ abtMe string
+
+ modernVotes int64
+ classicVotes int64
+ minimalVotes int64
+ currentTheme string
+
+ modernLink string
+ classicLink string
+ minimalLink string
+)
+
+func init() {
+ pfp = "https://static.artzone.ai/media/38734/conversions/IPF9dR7ro7n05CmMLLrXIojycr1qdLFxgutaaanG-w768.webp"
+ pfpCaption = "My profile picture - Tarantula Nebula"
+ abtMe = `Motivated Computer Science student with strong
+ analytical and problem-solving skills. Proficient in
+ programming and version control, with a high level of
+ focus and attention to detail. Eager to apply academic
+ knowledge to real-world projects and contribute to
+ innovative technology solutions.
+ In addition to my academic pursuits,
+ I enjoy traveling and staying active through weightlifting.
+ I have a keen interest in electronic music and often explore various genres.
+ I believe in maintaining a balanced lifestyle that complements my professional development.`
+
+ modernVotes = 0
+ classicVotes = 0
+ minimalVotes = 0
+ currentTheme = "classic"
+ modernLink = "https://www.google.com"
+ classicLink = "https://www.google.com"
+ minimalLink = "https://www.google.com"
+ hof.Register()
+}
+
+func UpdatePFP(url, caption string) {
+ AssertAuthorized()
+ pfp = url
+ pfpCaption = caption
+}
+
+func UpdateAboutMe(col1 string) {
+ AssertAuthorized()
+ abtMe = col1
+}
+
+func maxOfThree(a, b, c int64) int64 {
+ max := a
+ if b > max {
+ max = b
+ }
+ if c > max {
+ max = c
+ }
+ return max
+}
+
+func VoteModern() {
+ ugnotAmount := std.GetOrigSend().AmountOf("ugnot")
+ votes := ugnotAmount
+ modernVotes += votes
+ updateCurrentTheme()
+}
+
+func VoteClassic() {
+ ugnotAmount := std.GetOrigSend().AmountOf("ugnot")
+ votes := ugnotAmount
+ classicVotes += votes
+ updateCurrentTheme()
+}
+
+func VoteMinimal() {
+ ugnotAmount := std.GetOrigSend().AmountOf("ugnot")
+ votes := ugnotAmount
+ minimalVotes += votes
+ updateCurrentTheme()
+}
+
+func updateCurrentTheme() {
+ maxVotes := maxOfThree(modernVotes, classicVotes, minimalVotes)
+
+ if maxVotes == modernVotes {
+ currentTheme = "modern"
+ } else if maxVotes == classicVotes {
+ currentTheme = "classic"
+ } else {
+ currentTheme = "minimal"
+ }
+}
+
+func CollectBalance() {
+ AssertAuthorized()
+
+ banker := std.GetBanker(std.BankerTypeRealmSend)
+ ownerAddr := Address()
+
+ banker.SendCoins(std.CurrentRealm().Addr(), ownerAddr, banker.GetCoins(std.CurrentRealm().Addr()))
+}
+
+func Render(path string) string {
+ var sb strings.Builder
+
+ // Theme-specific header styling
+ switch currentTheme {
+ case "modern":
+ // Modern theme - Clean and minimalist with emojis
+ sb.WriteString(md.H1("🚀 Matija's Space"))
+ sb.WriteString(md.Image(pfpCaption, pfp))
+ sb.WriteString("\n")
+ sb.WriteString(md.Italic(pfpCaption))
+ sb.WriteString("\n")
+ sb.WriteString(md.HorizontalRule())
+ sb.WriteString(abtMe)
+ sb.WriteString("\n")
+
+ case "minimal":
+ // Minimal theme - No emojis, minimal formatting
+ sb.WriteString(md.H1("Matija Marjanovic"))
+ sb.WriteString("\n")
+ sb.WriteString(abtMe)
+ sb.WriteString("\n")
+ sb.WriteString(md.Image(pfpCaption, pfp))
+ sb.WriteString("\n")
+ sb.WriteString(pfpCaption)
+ sb.WriteString("\n")
+
+ default: // classic
+ // Classic theme - Traditional blog style with decorative elements
+ sb.WriteString(md.H1("✨ Welcome to Matija's Homepage ✨"))
+ sb.WriteString("\n")
+ sb.WriteString(md.Image(pfpCaption, pfp))
+ sb.WriteString("\n")
+ sb.WriteString(pfpCaption)
+ sb.WriteString("\n")
+ sb.WriteString(md.HorizontalRule())
+ sb.WriteString(md.H2("About me"))
+ sb.WriteString("\n")
+ sb.WriteString(abtMe)
+ sb.WriteString("\n")
+ }
+
+ // Theme-specific voting section
+ switch currentTheme {
+ case "modern":
+ sb.WriteString(md.HorizontalRule())
+ sb.WriteString(md.H2("🎨 Theme Selector"))
+ sb.WriteString("Choose your preferred viewing experience:\n")
+ items := []string{
+ md.Link(ufmt.Sprintf("Modern Design (%d votes)", modernVotes), modernLink),
+ md.Link(ufmt.Sprintf("Classic Style (%d votes)", classicVotes), classicLink),
+ md.Link(ufmt.Sprintf("Minimal Look (%d votes)", minimalVotes), minimalLink),
+ }
+ sb.WriteString(md.BulletList(items))
+
+ case "minimal":
+ sb.WriteString("\n")
+ sb.WriteString(md.H3("Theme Selection"))
+ sb.WriteString(ufmt.Sprintf("Current theme: %s\n", currentTheme))
+ sb.WriteString(ufmt.Sprintf("Votes - Modern: %d | Classic: %d | Minimal: %d\n",
+ modernVotes, classicVotes, minimalVotes))
+ sb.WriteString(md.Link("Modern", modernLink))
+ sb.WriteString(" | ")
+ sb.WriteString(md.Link("Classic", classicLink))
+ sb.WriteString(" | ")
+ sb.WriteString(md.Link("Minimal", minimalLink))
+ sb.WriteString("\n")
+
+ default: // classic
+ sb.WriteString(md.HorizontalRule())
+ sb.WriteString(md.H2("✨ Theme Customization ✨"))
+ sb.WriteString(md.Bold("Choose Your Preferred Theme:"))
+ sb.WriteString("\n\n")
+ items := []string{
+ ufmt.Sprintf("Modern 🚀 (%d votes) - %s", modernVotes, md.Link("Vote", modernLink)),
+ ufmt.Sprintf("Classic ✨ (%d votes) - %s", classicVotes, md.Link("Vote", classicLink)),
+ ufmt.Sprintf("Minimal ⚡ (%d votes) - %s", minimalVotes, md.Link("Vote", minimalLink)),
+ }
+ sb.WriteString(md.BulletList(items))
+ }
+
+ // Theme-specific footer/links section
+ switch currentTheme {
+ case "modern":
+ sb.WriteString(md.HorizontalRule())
+ sb.WriteString(md.Link("GitHub", "https://github.com/matijamarjanovic"))
+ sb.WriteString(" | ")
+ sb.WriteString(md.Link("LinkedIn", "https://www.linkedin.com/in/matijamarjanovic"))
+ sb.WriteString("\n")
+
+ case "minimal":
+ sb.WriteString("\n")
+ sb.WriteString(md.Link("GitHub", "https://github.com/matijamarjanovic"))
+ sb.WriteString(" | ")
+ sb.WriteString(md.Link("LinkedIn", "https://www.linkedin.com/in/matijamarjanovic"))
+ sb.WriteString("\n")
+
+ default: // classic
+ sb.WriteString(md.HorizontalRule())
+ sb.WriteString(md.H3("✨ Connect With Me"))
+ items := []string{
+ md.Link("🌟 GitHub", "https://github.com/matijamarjanovic"),
+ md.Link("💼 LinkedIn", "https://www.linkedin.com/in/matijamarjanovic"),
+ }
+ sb.WriteString(md.BulletList(items))
+ }
+
+ return sb.String()
+}
+
+func UpdateModernLink(link string) {
+ AssertAuthorized()
+ modernLink = link
+}
+
+func UpdateClassicLink(link string) {
+ AssertAuthorized()
+ classicLink = link
+}
+
+func UpdateMinimalLink(link string) {
+ AssertAuthorized()
+ minimalLink = link
+}
diff --git a/examples/gno.land/r/matijamarjanovic/home/home_test.gno b/examples/gno.land/r/matijamarjanovic/home/home_test.gno
new file mode 100644
index 00000000000..8cc6e6e5608
--- /dev/null
+++ b/examples/gno.land/r/matijamarjanovic/home/home_test.gno
@@ -0,0 +1,134 @@
+package home
+
+import (
+ "std"
+ "strings"
+ "testing"
+
+ "gno.land/p/demo/uassert"
+ "gno.land/p/demo/urequire"
+)
+
+// Helper function to set up test environment
+func setupTest() {
+ std.TestSetOrigCaller(std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y"))
+}
+
+func TestUpdatePFP(t *testing.T) {
+ setupTest()
+ pfp = ""
+ pfpCaption = ""
+
+ UpdatePFP("https://example.com/pic.png", "New Caption")
+
+ urequire.Equal(t, pfp, "https://example.com/pic.png", "Profile picture URL should be updated")
+ urequire.Equal(t, pfpCaption, "New Caption", "Profile picture caption should be updated")
+}
+
+func TestUpdateAboutMe(t *testing.T) {
+ setupTest()
+ abtMe = ""
+
+ UpdateAboutMe("This is my new bio.")
+
+ urequire.Equal(t, abtMe, "This is my new bio.", "About Me should be updated")
+}
+
+func TestVoteModern(t *testing.T) {
+ setupTest()
+ modernVotes, classicVotes, minimalVotes = 0, 0, 0
+
+ coinsSent := std.NewCoins(std.NewCoin("ugnot", 75000000))
+ coinsSpent := std.NewCoins(std.NewCoin("ugnot", 1))
+
+ std.TestSetOrigSend(coinsSent, coinsSpent)
+ VoteModern()
+
+ uassert.Equal(t, int64(75000000), modernVotes, "Modern votes should be calculated correctly")
+ uassert.Equal(t, "modern", currentTheme, "Theme should be updated to modern")
+}
+
+func TestVoteClassic(t *testing.T) {
+ setupTest()
+ modernVotes, classicVotes, minimalVotes = 0, 0, 0
+
+ coinsSent := std.NewCoins(std.NewCoin("ugnot", 75000000))
+ coinsSpent := std.NewCoins(std.NewCoin("ugnot", 1))
+
+ std.TestSetOrigSend(coinsSent, coinsSpent)
+ VoteClassic()
+
+ uassert.Equal(t, int64(75000000), classicVotes, "Classic votes should be calculated correctly")
+ uassert.Equal(t, "classic", currentTheme, "Theme should be updated to classic")
+}
+
+func TestVoteMinimal(t *testing.T) {
+ setupTest()
+ modernVotes, classicVotes, minimalVotes = 0, 0, 0
+
+ coinsSent := std.NewCoins(std.NewCoin("ugnot", 75000000))
+ coinsSpent := std.NewCoins(std.NewCoin("ugnot", 1))
+
+ std.TestSetOrigSend(coinsSent, coinsSpent)
+ VoteMinimal()
+
+ uassert.Equal(t, int64(75000000), minimalVotes, "Minimal votes should be calculated correctly")
+ uassert.Equal(t, "minimal", currentTheme, "Theme should be updated to minimal")
+}
+
+func TestRender(t *testing.T) {
+ setupTest()
+ // Reset the state to known values
+ modernVotes, classicVotes, minimalVotes = 0, 0, 0
+ currentTheme = "classic"
+ pfp = "https://example.com/pic.png"
+ pfpCaption = "Test Caption"
+ abtMe = "Test About Me"
+
+ out := Render("")
+ urequire.NotEqual(t, out, "", "Render output should not be empty")
+
+ // Test classic theme specific content
+ uassert.True(t, strings.Contains(out, "✨ Welcome to Matija's Homepage ✨"), "Classic theme should have correct header")
+ uassert.True(t, strings.Contains(out, pfp), "Should contain profile picture URL")
+ uassert.True(t, strings.Contains(out, pfpCaption), "Should contain profile picture caption")
+ uassert.True(t, strings.Contains(out, "About me"), "Should contain About me section")
+ uassert.True(t, strings.Contains(out, abtMe), "Should contain about me content")
+ uassert.True(t, strings.Contains(out, "Theme Customization"), "Should contain theme customization section")
+ uassert.True(t, strings.Contains(out, "Connect With Me"), "Should contain connect section")
+}
+
+func TestRenderModernTheme(t *testing.T) {
+ setupTest()
+ modernVotes, classicVotes, minimalVotes = 100, 0, 0
+ currentTheme = "modern"
+ updateCurrentTheme()
+
+ out := Render("")
+ uassert.True(t, strings.Contains(out, "🚀 Matija's Space"), "Modern theme should have correct header")
+}
+
+func TestRenderMinimalTheme(t *testing.T) {
+ setupTest()
+ modernVotes, classicVotes, minimalVotes = 0, 0, 100
+ currentTheme = "minimal"
+ updateCurrentTheme()
+
+ out := Render("")
+ uassert.True(t, strings.Contains(out, "Matija Marjanovic"), "Minimal theme should have correct header")
+}
+
+func TestUpdateLinks(t *testing.T) {
+ setupTest()
+
+ newLink := "https://example.com/vote"
+
+ UpdateModernLink(newLink)
+ urequire.Equal(t, modernLink, newLink, "Modern link should be updated")
+
+ UpdateClassicLink(newLink)
+ urequire.Equal(t, classicLink, newLink, "Classic link should be updated")
+
+ UpdateMinimalLink(newLink)
+ urequire.Equal(t, minimalLink, newLink, "Minimal link should be updated")
+}
diff --git a/examples/gno.land/r/morgan/guestbook/admin.gno b/examples/gno.land/r/morgan/guestbook/admin.gno
new file mode 100644
index 00000000000..fb7f9e1461c
--- /dev/null
+++ b/examples/gno.land/r/morgan/guestbook/admin.gno
@@ -0,0 +1,25 @@
+package guestbook
+
+import (
+ "gno.land/p/demo/ownable"
+ "gno.land/p/demo/seqid"
+)
+
+var owner = ownable.New()
+
+// AdminDelete removes the guestbook message with the given ID.
+// The user will still be marked as having submitted a message, so they
+// won't be able to re-submit a new message.
+func AdminDelete(signatureID string) {
+ owner.AssertCallerIsOwner()
+
+ id, err := seqid.FromString(signatureID)
+ if err != nil {
+ panic(err)
+ }
+ idb := id.Binary()
+ if !guestbook.Has(idb) {
+ panic("signature does not exist")
+ }
+ guestbook.Remove(idb)
+}
diff --git a/examples/gno.land/r/morgan/guestbook/gno.mod b/examples/gno.land/r/morgan/guestbook/gno.mod
new file mode 100644
index 00000000000..ac63a4cf8cd
--- /dev/null
+++ b/examples/gno.land/r/morgan/guestbook/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/morgan/guestbook
diff --git a/examples/gno.land/r/morgan/guestbook/guestbook.gno b/examples/gno.land/r/morgan/guestbook/guestbook.gno
new file mode 100644
index 00000000000..be9e9db6133
--- /dev/null
+++ b/examples/gno.land/r/morgan/guestbook/guestbook.gno
@@ -0,0 +1,126 @@
+// Realm guestbook contains an implementation of a simple guestbook.
+// Come and sign yourself up!
+package guestbook
+
+import (
+ "std"
+ "strconv"
+ "strings"
+ "time"
+ "unicode"
+
+ "gno.land/p/demo/avl"
+ "gno.land/p/demo/seqid"
+)
+
+// Signature is a single entry in the guestbook.
+type Signature struct {
+ Message string
+ Author std.Address
+ Time time.Time
+}
+
+const (
+ maxMessageLength = 140
+ maxPerPage = 25
+)
+
+var (
+ signatureID seqid.ID
+ guestbook avl.Tree // id -> Signature
+ hasSigned avl.Tree // address -> struct{}
+)
+
+func init() {
+ Sign("You reached the end of the guestbook!")
+}
+
+const (
+ errNotAUser = "this guestbook can only be signed by users"
+ errAlreadySigned = "you already signed the guestbook!"
+ errInvalidCharacterInMessage = "invalid character in message"
+)
+
+// Sign signs the guestbook, with the specified message.
+func Sign(message string) {
+ prev := std.PrevRealm()
+ switch {
+ case !prev.IsUser():
+ panic(errNotAUser)
+ case hasSigned.Has(prev.Addr().String()):
+ panic(errAlreadySigned)
+ }
+ message = validateMessage(message)
+
+ guestbook.Set(signatureID.Next().Binary(), Signature{
+ Message: message,
+ Author: prev.Addr(),
+ // NOTE: time.Now() will yield the "block time", which is deterministic.
+ Time: time.Now(),
+ })
+ hasSigned.Set(prev.Addr().String(), struct{}{})
+}
+
+func validateMessage(msg string) string {
+ if len(msg) > maxMessageLength {
+ panic("Keep it brief! (max " + strconv.Itoa(maxMessageLength) + " bytes!)")
+ }
+ out := ""
+ for _, ch := range msg {
+ switch {
+ case unicode.IsLetter(ch),
+ unicode.IsNumber(ch),
+ unicode.IsSpace(ch),
+ unicode.IsPunct(ch):
+ out += string(ch)
+ default:
+ panic(errInvalidCharacterInMessage)
+ }
+ }
+ return out
+}
+
+func Render(maxID string) string {
+ var bld strings.Builder
+
+ bld.WriteString("# Guestbook 📝\n\n[Come sign the guestbook!](./guestbook$help&func=Sign)\n\n---\n\n")
+
+ var maxIDBinary string
+ if maxID != "" {
+ mid, err := seqid.FromString(maxID)
+ if err != nil {
+ panic(err)
+ }
+
+ // AVL iteration is exclusive, so we need to decrease the ID value to get the "true" maximum.
+ mid--
+ maxIDBinary = mid.Binary()
+ }
+
+ var lastID seqid.ID
+ var printed int
+ guestbook.ReverseIterate("", maxIDBinary, func(key string, val interface{}) bool {
+ sig := val.(Signature)
+ message := strings.ReplaceAll(sig.Message, "\n", "\n> ")
+ bld.WriteString("> " + message + "\n>\n")
+ idValue, ok := seqid.FromBinary(key)
+ if !ok {
+ panic("invalid seqid id")
+ }
+
+ bld.WriteString("> _Written by " + sig.Author.String() + " at " + sig.Time.Format(time.DateTime) + "_ (#" + idValue.String() + ")\n\n---\n\n")
+ lastID = idValue
+
+ printed++
+ // stop after exceeding limit
+ return printed >= maxPerPage
+ })
+
+ if printed == 0 {
+ bld.WriteString("No messages!")
+ } else if printed >= maxPerPage {
+ bld.WriteString("
Next page
")
+ }
+
+ return bld.String()
+}
diff --git a/examples/gno.land/r/morgan/guestbook/guestbook_test.gno b/examples/gno.land/r/morgan/guestbook/guestbook_test.gno
new file mode 100644
index 00000000000..b14fee45b42
--- /dev/null
+++ b/examples/gno.land/r/morgan/guestbook/guestbook_test.gno
@@ -0,0 +1,131 @@
+package guestbook
+
+import (
+ "std"
+ "strings"
+ "testing"
+
+ "gno.land/p/demo/avl"
+ "gno.land/p/demo/ownable"
+)
+
+func TestSign(t *testing.T) {
+ guestbook = avl.Tree{}
+ hasSigned = avl.Tree{}
+
+ std.TestSetRealm(std.NewUserRealm("g1user"))
+ Sign("Hello!")
+
+ std.TestSetRealm(std.NewUserRealm("g1user2"))
+ Sign("Hello2!")
+
+ res := Render("")
+ t.Log(res)
+ if !strings.Contains(res, "> Hello!\n>\n> _Written by g1user ") {
+ t.Error("does not contain first user's message")
+ }
+ if !strings.Contains(res, "> Hello2!\n>\n> _Written by g1user2 ") {
+ t.Error("does not contain second user's message")
+ }
+ if guestbook.Size() != 2 {
+ t.Error("invalid guestbook size")
+ }
+}
+
+func TestSign_FromRealm(t *testing.T) {
+ std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/users"))
+
+ defer func() {
+ rec := recover()
+ if rec == nil {
+ t.Fatal("expected panic")
+ }
+ recString, ok := rec.(string)
+ if !ok {
+ t.Fatal("not a string", rec)
+ } else if recString != errNotAUser {
+ t.Fatal("invalid error", recString)
+ }
+ }()
+ Sign("Hey!")
+}
+
+func TestSign_Double(t *testing.T) {
+ // Should not allow signing twice.
+ guestbook = avl.Tree{}
+ hasSigned = avl.Tree{}
+
+ std.TestSetRealm(std.NewUserRealm("g1user"))
+ Sign("Hello!")
+
+ defer func() {
+ rec := recover()
+ if rec == nil {
+ t.Fatal("expected panic")
+ }
+ recString, ok := rec.(string)
+ if !ok {
+ t.Error("type assertion failed", rec)
+ } else if recString != errAlreadySigned {
+ t.Error("invalid error message", recString)
+ }
+ }()
+
+ Sign("Hello again!")
+}
+
+func TestSign_InvalidMessage(t *testing.T) {
+ // Should not allow control characters in message.
+ guestbook = avl.Tree{}
+ hasSigned = avl.Tree{}
+
+ std.TestSetRealm(std.NewUserRealm("g1user"))
+
+ defer func() {
+ rec := recover()
+ if rec == nil {
+ t.Fatal("expected panic")
+ }
+ recString, ok := rec.(string)
+ if !ok {
+ t.Error("type assertion failed", rec)
+ } else if recString != errInvalidCharacterInMessage {
+ t.Error("invalid error message", recString)
+ }
+ }()
+ Sign("\x00Hello!")
+}
+
+func TestAdminDelete(t *testing.T) {
+ const (
+ userAddr std.Address = "g1user"
+ adminAddr std.Address = "g1admin"
+ )
+
+ guestbook = avl.Tree{}
+ hasSigned = avl.Tree{}
+ owner = ownable.NewWithAddress(adminAddr)
+ signatureID = 0
+
+ std.TestSetRealm(std.NewUserRealm(userAddr))
+
+ const bad = "Very Bad Message! Nyeh heh heh!"
+ Sign(bad)
+
+ if rnd := Render(""); !strings.Contains(rnd, bad) {
+ t.Fatal("render does not contain bad message", rnd)
+ }
+
+ std.TestSetRealm(std.NewUserRealm(adminAddr))
+ AdminDelete(signatureID.String())
+
+ if rnd := Render(""); strings.Contains(rnd, bad) {
+ t.Error("render contains bad message", rnd)
+ }
+ if guestbook.Size() != 0 {
+ t.Error("invalid guestbook size")
+ }
+ if hasSigned.Size() != 1 {
+ t.Error("invalid hasSigned size")
+ }
+}
diff --git a/examples/gno.land/r/morgan/home/gno.mod b/examples/gno.land/r/morgan/home/gno.mod
new file mode 100644
index 00000000000..573a7e139e7
--- /dev/null
+++ b/examples/gno.land/r/morgan/home/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/morgan/home
diff --git a/examples/gno.land/r/morgan/home/home.gno b/examples/gno.land/r/morgan/home/home.gno
new file mode 100644
index 00000000000..20b66b895e3
--- /dev/null
+++ b/examples/gno.land/r/morgan/home/home.gno
@@ -0,0 +1,14 @@
+package home
+
+import "gno.land/r/leon/hof"
+
+const staticHome = `# morgan's (gn)home
+
+- [📝 sign my guestbook](/r/morgan/guestbook)
+`
+
+func init() { hof.Register() }
+
+func Render(path string) string {
+ return staticHome
+}
diff --git a/examples/gno.land/r/manfred/README.md b/examples/gno.land/r/moul/README.md
similarity index 100%
rename from examples/gno.land/r/manfred/README.md
rename to examples/gno.land/r/moul/README.md
diff --git a/examples/gno.land/r/moul/config/config.gno b/examples/gno.land/r/moul/config/config.gno
new file mode 100644
index 00000000000..a4f24411747
--- /dev/null
+++ b/examples/gno.land/r/moul/config/config.gno
@@ -0,0 +1,20 @@
+package config
+
+import "std"
+
+var addr = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") // @moul
+
+func Addr() std.Address {
+ return addr
+}
+
+func UpdateAddr(newAddr std.Address) {
+ AssertIsAdmin()
+ addr = newAddr
+}
+
+func AssertIsAdmin() {
+ if std.GetOrigCaller() != addr {
+ panic("restricted area")
+ }
+}
diff --git a/examples/gno.land/r/moul/config/config_test.gno b/examples/gno.land/r/moul/config/config_test.gno
new file mode 100644
index 00000000000..d912156bec0
--- /dev/null
+++ b/examples/gno.land/r/moul/config/config_test.gno
@@ -0,0 +1 @@
+package config
diff --git a/examples/gno.land/r/moul/config/gno.mod b/examples/gno.land/r/moul/config/gno.mod
new file mode 100644
index 00000000000..2029efc8fcb
--- /dev/null
+++ b/examples/gno.land/r/moul/config/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/moul/config
diff --git a/examples/gno.land/r/moul/home/gno.mod b/examples/gno.land/r/moul/home/gno.mod
new file mode 100644
index 00000000000..91e02df3707
--- /dev/null
+++ b/examples/gno.land/r/moul/home/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/moul/home
diff --git a/examples/gno.land/r/moul/home/home.gno b/examples/gno.land/r/moul/home/home.gno
new file mode 100644
index 00000000000..1094ce29cc5
--- /dev/null
+++ b/examples/gno.land/r/moul/home/home.gno
@@ -0,0 +1,107 @@
+package home
+
+import (
+ "strconv"
+
+ "gno.land/p/demo/svg"
+ "gno.land/p/moul/debug"
+ "gno.land/p/moul/md"
+ "gno.land/p/moul/mdtable"
+ "gno.land/p/moul/txlink"
+ "gno.land/p/moul/web25"
+ "gno.land/r/leon/hof"
+ "gno.land/r/moul/config"
+)
+
+var (
+ todos []string
+ status string
+ memeImgURL string
+ web25config = web25.Config{URL: "https://moul.github.io/gno-moul-home-web25/"}
+)
+
+func init() {
+ todos = append(todos, "fill this todo list...")
+ status = "Online" // Initial status set to "Online"
+ memeImgURL = "https://i.imgflip.com/7ze8dc.jpg"
+ hof.Register()
+}
+
+func Render(path string) string {
+ content := web25config.Render(path)
+ var d debug.Debug
+
+ content += md.H1("Manfred's (gn)home Dashboard")
+
+ content += md.H2("Meme")
+ content += md.Paragraph(
+ md.Image("meme", memeImgURL),
+ )
+
+ content += md.H2("Status")
+ content += md.Paragraph(status)
+ content += md.Paragraph(md.Link("update", txlink.Call("UpdateStatus")))
+
+ d.Log("hello world!")
+
+ content += md.H2("Personal TODO List (bullet list)")
+ for i, todo := range todos {
+ idstr := strconv.Itoa(i)
+ deleteLink := md.Link("x", txlink.Call("DeleteTodo", "idx", idstr))
+ content += md.BulletItem(todo + " " + deleteLink)
+ }
+ content += md.BulletItem(md.Link("[new]", txlink.Call("AddTodo")))
+
+ content += md.H2("Personal TODO List (table)")
+ table := mdtable.Table{
+ Headers: []string{"ID", "Item", "Links"},
+ }
+ for i, todo := range todos {
+ idstr := strconv.Itoa(i)
+ deleteLink := md.Link("[del]", txlink.Call("DeleteTodo", "idx", idstr))
+ table.Append([]string{"#" + idstr, todo, deleteLink})
+ }
+ content += table.String()
+
+ content += md.H2("SVG Example")
+ content += md.Paragraph("this feature may not work with the current gnoweb version and/or configuration.")
+ content += md.Paragraph(svg.Canvas{
+ Width: 500, Height: 500,
+ Elems: []svg.Elem{
+ svg.Rectangle{50, 50, 100, 100, "red"},
+ svg.Circle{50, 50, 100, "red"},
+ svg.Text{100, 100, "hello world!", "magenta"},
+ },
+ }.String())
+
+ content += md.H2("Debug")
+ content += md.Paragraph("this feature may not work with the current gnoweb version and/or configuration.")
+ content += md.Paragraph(
+ md.Link("toggle debug", debug.ToggleURL(path)),
+ )
+
+ // TODO: my r/boards posts
+ // TODO: my r/events events
+ content += d.Render(path)
+ return content
+}
+
+func AddTodo(todo string) {
+ config.AssertIsAdmin()
+ todos = append(todos, todo)
+}
+
+func DeleteTodo(idx int) {
+ config.AssertIsAdmin()
+ if idx >= 0 && idx < len(todos) {
+ // Remove the todo from the list by merging slices from before and after the todo
+ todos = append(todos[:idx], todos[idx+1:]...)
+ } else {
+ panic("Invalid todo index")
+ }
+}
+
+func UpdateStatus(newStatus string) {
+ config.AssertIsAdmin()
+ status = newStatus
+}
diff --git a/examples/gno.land/r/moul/home/z1_filetest.gno b/examples/gno.land/r/moul/home/z1_filetest.gno
new file mode 100644
index 00000000000..b9d7d91a702
--- /dev/null
+++ b/examples/gno.land/r/moul/home/z1_filetest.gno
@@ -0,0 +1,37 @@
+package main
+
+import "gno.land/r/moul/home"
+
+func main() {
+ println(home.Render(""))
+}
+
+// Output:
+// Click [here](https://moul.github.io/gno-moul-home-web25/) to visit the full rendering experience.
+// # Manfred's (gn)home Dashboard
+// ## Meme
+// ![meme](https://i.imgflip.com/7ze8dc.jpg)
+//
+// ## Status
+// Online
+//
+// [update](/r/moul/home$help&func=UpdateStatus)
+//
+// ## Personal TODO List (bullet list)
+// - fill this todo list... [x](/r/moul/home$help&func=DeleteTodo&idx=0)
+// - [\[new\]](/r/moul/home$help&func=AddTodo)
+// ## Personal TODO List (table)
+// | ID | Item | Links |
+// | --- | --- | --- |
+// | #0 | fill this todo list... | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=0) |
+// ## SVG Example
+// this feature may not work with the current gnoweb version and/or configuration.
+//
+//
hello world!
+//
+// ## Debug
+// this feature may not work with the current gnoweb version and/or configuration.
+//
+// [toggle debug](/r/moul/home:?debug=1)
+//
+//
diff --git a/examples/gno.land/r/moul/home/z2_filetest.gno b/examples/gno.land/r/moul/home/z2_filetest.gno
new file mode 100644
index 00000000000..f471280d8ef
--- /dev/null
+++ b/examples/gno.land/r/moul/home/z2_filetest.gno
@@ -0,0 +1,72 @@
+package main
+
+import (
+ "std"
+
+ "gno.land/r/moul/home"
+)
+
+func main() {
+ std.TestSetOrigCaller("g1manfred47kzduec920z88wfr64ylksmdcedlf5")
+ home.AddTodo("aaa")
+ home.AddTodo("bbb")
+ home.AddTodo("ccc")
+ home.AddTodo("ddd")
+ home.AddTodo("eee")
+ home.UpdateStatus("Lorem Ipsum")
+ home.DeleteTodo(3)
+ println(home.Render("?debug=1"))
+}
+
+// Output:
+// Click [here](https://moul.github.io/gno-moul-home-web25/) to visit the full rendering experience.
+// # Manfred's (gn)home Dashboard
+// ## Meme
+// ![meme](https://i.imgflip.com/7ze8dc.jpg)
+//
+// ## Status
+// Lorem Ipsum
+//
+// [update](/r/moul/home$help&func=UpdateStatus)
+//
+// ## Personal TODO List (bullet list)
+// - fill this todo list... [x](/r/moul/home$help&func=DeleteTodo&idx=0)
+// - aaa [x](/r/moul/home$help&func=DeleteTodo&idx=1)
+// - bbb [x](/r/moul/home$help&func=DeleteTodo&idx=2)
+// - ddd [x](/r/moul/home$help&func=DeleteTodo&idx=3)
+// - eee [x](/r/moul/home$help&func=DeleteTodo&idx=4)
+// - [\[new\]](/r/moul/home$help&func=AddTodo)
+// ## Personal TODO List (table)
+// | ID | Item | Links |
+// | --- | --- | --- |
+// | #0 | fill this todo list... | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=0) |
+// | #1 | aaa | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=1) |
+// | #2 | bbb | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=2) |
+// | #3 | ddd | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=3) |
+// | #4 | eee | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=4) |
+// ## SVG Example
+// this feature may not work with the current gnoweb version and/or configuration.
+//
+//
hello world!
+//
+// ## Debug
+// this feature may not work with the current gnoweb version and/or configuration.
+//
+// [toggle debug](/r/moul/home:)
+//
+//
debug
+//
+// ### Logs
+// - hello world!
+// ### Metadata
+// | Key | Value |
+// | --- | --- |
+// | `std.CurrentRealm().PkgPath()` | gno.land/r/moul/home |
+// | `std.CurrentRealm().Addr()` | g1h8h57ntxadcze3f703skymfzdwa6t3ugf0nq3z |
+// | `std.PrevRealm().PkgPath()` | |
+// | `std.PrevRealm().Addr()` | g1manfred47kzduec920z88wfr64ylksmdcedlf5 |
+// | `std.GetHeight()` | 123 |
+// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z |
+//
+//
+//
diff --git a/examples/gno.land/r/moul/present/admin.gno b/examples/gno.land/r/moul/present/admin.gno
new file mode 100644
index 00000000000..ab99b1725c5
--- /dev/null
+++ b/examples/gno.land/r/moul/present/admin.gno
@@ -0,0 +1,96 @@
+package present
+
+import (
+ "std"
+ "strings"
+
+ "gno.land/p/demo/avl"
+)
+
+var (
+ adminAddr std.Address
+ moderatorList avl.Tree
+ inPause bool
+)
+
+func init() {
+ // adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis.
+ adminAddr = "g1manfred47kzduec920z88wfr64ylksmdcedlf5"
+}
+
+func AdminSetAdminAddr(addr std.Address) {
+ assertIsAdmin()
+ adminAddr = addr
+}
+
+func AdminSetInPause(state bool) {
+ assertIsAdmin()
+ inPause = state
+}
+
+func AdminAddModerator(addr std.Address) {
+ assertIsAdmin()
+ moderatorList.Set(addr.String(), true)
+}
+
+func AdminRemoveModerator(addr std.Address) {
+ assertIsAdmin()
+ moderatorList.Set(addr.String(), false) // XXX: delete instead?
+}
+
+func ModAddPost(slug, title, body, publicationDate, authors, tags string) {
+ assertIsModerator()
+
+ caller := std.GetOrigCaller()
+ tagList := strings.Split(tags, ",")
+ authorList := strings.Split(authors, ",")
+
+ err := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList)
+ checkErr(err)
+}
+
+func ModEditPost(slug, title, body, publicationDate, authors, tags string) {
+ assertIsModerator()
+
+ tagList := strings.Split(tags, ",")
+ authorList := strings.Split(authors, ",")
+
+ err := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList)
+ checkErr(err)
+}
+
+func isAdmin(addr std.Address) bool {
+ return addr == adminAddr
+}
+
+func isModerator(addr std.Address) bool {
+ _, found := moderatorList.Get(addr.String())
+ return found
+}
+
+func assertIsAdmin() {
+ caller := std.GetOrigCaller()
+ if !isAdmin(caller) {
+ panic("access restricted.")
+ }
+}
+
+func assertIsModerator() {
+ caller := std.GetOrigCaller()
+ if isAdmin(caller) || isModerator(caller) {
+ return
+ }
+ panic("access restricted")
+}
+
+func assertNotInPause() {
+ if inPause {
+ panic("access restricted (pause)")
+ }
+}
+
+func checkErr(err error) {
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/examples/gno.land/r/moul/present/gno.mod b/examples/gno.land/r/moul/present/gno.mod
new file mode 100644
index 00000000000..a0a7777d0ed
--- /dev/null
+++ b/examples/gno.land/r/moul/present/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/moul/present
diff --git a/examples/gno.land/r/manfred/present/present_miami23.gno b/examples/gno.land/r/moul/present/present_miami23.gno
similarity index 100%
rename from examples/gno.land/r/manfred/present/present_miami23.gno
rename to examples/gno.land/r/moul/present/present_miami23.gno
diff --git a/examples/gno.land/r/manfred/present/present_miami23_filetest.gno b/examples/gno.land/r/moul/present/present_miami23_filetest.gno
similarity index 84%
rename from examples/gno.land/r/manfred/present/present_miami23_filetest.gno
rename to examples/gno.land/r/moul/present/present_miami23_filetest.gno
index ac19d83ade4..09d332ec6e4 100644
--- a/examples/gno.land/r/manfred/present/present_miami23_filetest.gno
+++ b/examples/gno.land/r/moul/present/present_miami23_filetest.gno
@@ -1,7 +1,7 @@
package main
import (
- "gno.land/r/manfred/present"
+ "gno.land/r/moul/present"
)
func main() {
diff --git a/examples/gno.land/r/manfred/present/presentations.gno b/examples/gno.land/r/moul/present/presentations.gno
similarity index 86%
rename from examples/gno.land/r/manfred/present/presentations.gno
rename to examples/gno.land/r/moul/present/presentations.gno
index 8a99f502e86..c5529804751 100644
--- a/examples/gno.land/r/manfred/present/presentations.gno
+++ b/examples/gno.land/r/moul/present/presentations.gno
@@ -8,7 +8,7 @@ import (
var b = &blog.Blog{
Title: "Manfred's Presentations",
- Prefix: "/r/manfred/present:",
+ Prefix: "/r/moul/present:",
NoBreadcrumb: true,
}
diff --git a/examples/gno.land/r/n2p5/config/config.gno b/examples/gno.land/r/n2p5/config/config.gno
new file mode 100644
index 00000000000..42cb587eaf5
--- /dev/null
+++ b/examples/gno.land/r/n2p5/config/config.gno
@@ -0,0 +1,120 @@
+package config
+
+import (
+ "std"
+
+ "gno.land/p/demo/ufmt"
+ "gno.land/p/n2p5/mgroup"
+)
+
+const (
+ originalOwner = "g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t" // n2p5
+)
+
+var (
+ adminGroup = mgroup.New(originalOwner)
+ description = ""
+)
+
+// AddBackupOwner adds a backup owner to the Owner Group.
+// A backup owner can claim ownership of the contract.
+func AddBackupOwner(addr std.Address) {
+ err := adminGroup.AddBackupOwner(addr)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// RemoveBackupOwner removes a backup owner from the Owner Group.
+// The primary owner cannot be removed.
+func RemoveBackupOwner(addr std.Address) {
+ err := adminGroup.RemoveBackupOwner(addr)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// ClaimOwnership allows an authorized user in the ownerGroup
+// to claim ownership of the contract.
+func ClaimOwnership() {
+ err := adminGroup.ClaimOwnership()
+ if err != nil {
+ panic(err)
+ }
+}
+
+// AddAdmin adds an admin to the Admin Group.
+func AddAdmin(addr std.Address) {
+ err := adminGroup.AddMember(addr)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// RemoveAdmin removes an admin from the Admin Group.
+// The primary owner cannot be removed.
+func RemoveAdmin(addr std.Address) {
+ err := adminGroup.RemoveMember(addr)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// Owner returns the current owner of the claims contract.
+func Owner() std.Address {
+ return adminGroup.Owner()
+}
+
+// BackupOwners returns the current backup owners of the claims contract.
+func BackupOwners() []string {
+ return adminGroup.BackupOwners()
+}
+
+// Admins returns the current admin members of the claims contract.
+func Admins() []string {
+ return adminGroup.Members()
+}
+
+// IsAdmin checks if an address is in the config adminGroup.
+func IsAdmin(addr std.Address) bool {
+ return adminGroup.IsMember(addr)
+}
+
+// toMarkdownList formats a slice of strings as a markdown list.
+func toMarkdownList(items []string) string {
+ var result string
+ for _, item := range items {
+ result += ufmt.Sprintf("- %s\n", item)
+ }
+ return result
+}
+
+func Render(path string) string {
+ owner := adminGroup.Owner().String()
+ backupOwners := toMarkdownList(BackupOwners())
+ adminMembers := toMarkdownList(Admins())
+ return ufmt.Sprintf(`
+# Config Dashboard
+
+This dashboard shows the current configuration owner, backup owners, and admin members.
+- The owner has the exclusive ability to manage the backup owners and admin members.
+- Backup owners can claim ownership of the contract and become the owner.
+- Admin members are used to authorize actions in other realms, such as [my home realm](/r/n2p5/home).
+
+#### Owner
+
+%s
+
+#### Backup Owners
+
+%s
+
+#### Admin Members
+
+%s
+
+`,
+ owner,
+ backupOwners,
+ adminMembers)
+}
diff --git a/examples/gno.land/r/n2p5/config/gno.mod b/examples/gno.land/r/n2p5/config/gno.mod
new file mode 100644
index 00000000000..29d5a74eb0a
--- /dev/null
+++ b/examples/gno.land/r/n2p5/config/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/n2p5/config
diff --git a/examples/gno.land/r/n2p5/haystack/gno.mod b/examples/gno.land/r/n2p5/haystack/gno.mod
new file mode 100644
index 00000000000..17c131b8370
--- /dev/null
+++ b/examples/gno.land/r/n2p5/haystack/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/n2p5/haystack
diff --git a/examples/gno.land/r/n2p5/haystack/haystack.gno b/examples/gno.land/r/n2p5/haystack/haystack.gno
new file mode 100644
index 00000000000..397de1e3e3d
--- /dev/null
+++ b/examples/gno.land/r/n2p5/haystack/haystack.gno
@@ -0,0 +1,32 @@
+package haystack
+
+import (
+ "gno.land/p/n2p5/haystack"
+)
+
+var storage = haystack.New()
+
+func Render(path string) string {
+ return `
+Put a Needle in the Haystack.
+`
+}
+
+// Add takes a fixed-length hex-encoded needle bytes and adds it to the haystack key-value store.
+// If storage encounters an error, it will panic.
+func Add(needleHex string) {
+ err := storage.Add(needleHex)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// Get takes a fixed-length hex-encoded needle hash and returns the hex-encoded needle bytes.
+// If storage encounters an error, it will panic.
+func Get(hashHex string) string {
+ needleHex, err := storage.Get(hashHex)
+ if err != nil {
+ panic(err)
+ }
+ return needleHex
+}
diff --git a/examples/gno.land/r/n2p5/haystack/haystack_test.gno b/examples/gno.land/r/n2p5/haystack/haystack_test.gno
new file mode 100644
index 00000000000..52dadf8bf9e
--- /dev/null
+++ b/examples/gno.land/r/n2p5/haystack/haystack_test.gno
@@ -0,0 +1,70 @@
+package haystack
+
+import (
+ "encoding/hex"
+ "std"
+ "testing"
+
+ "gno.land/p/demo/testutils"
+ "gno.land/p/demo/urequire"
+ "gno.land/p/n2p5/haystack"
+ "gno.land/p/n2p5/haystack/needle"
+)
+
+func TestHaystack(t *testing.T) {
+ t.Parallel()
+ // needleHex returns a hex-encoded needle and its hash for a given index.
+ genNeedleHex := func(i int) (string, string) {
+ b := make([]byte, needle.PayloadLength)
+ b[0] = byte(i)
+ n, _ := needle.New(b)
+ return hex.EncodeToString(n.Bytes()), hex.EncodeToString(n.Hash())
+ }
+
+ u1 := testutils.TestAddress("u1")
+ u2 := testutils.TestAddress("u2")
+
+ t.Run("Add", func(t *testing.T) {
+ t.Parallel()
+
+ n1, _ := genNeedleHex(1)
+ n2, _ := genNeedleHex(2)
+ n3, _ := genNeedleHex(3)
+
+ std.TestSetOrigCaller(u1)
+ urequire.NotPanics(t, func() { Add(n1) })
+ urequire.PanicsWithMessage(t,
+ haystack.ErrorDuplicateNeedle.Error(),
+ func() {
+ Add(n1)
+ })
+ std.TestSetOrigCaller(u2)
+ urequire.NotPanics(t, func() { Add(n2) })
+ urequire.NotPanics(t, func() { Add(n3) })
+ })
+
+ t.Run("Get", func(t *testing.T) {
+ t.Parallel()
+
+ n1, h1 := genNeedleHex(4)
+ _, h2 := genNeedleHex(5)
+
+ std.TestSetOrigCaller(u1)
+ urequire.NotPanics(t, func() { Add(n1) })
+ urequire.NotPanics(t, func() {
+ result := Get(h1)
+ urequire.Equal(t, n1, result)
+ })
+
+ std.TestSetOrigCaller(u2)
+ urequire.NotPanics(t, func() {
+ result := Get(h1)
+ urequire.Equal(t, n1, result)
+ })
+ urequire.PanicsWithMessage(t,
+ haystack.ErrorNeedleNotFound.Error(),
+ func() {
+ Get(h2)
+ })
+ })
+}
diff --git a/examples/gno.land/r/n2p5/home/gno.mod b/examples/gno.land/r/n2p5/home/gno.mod
new file mode 100644
index 00000000000..3b6ddbf86bb
--- /dev/null
+++ b/examples/gno.land/r/n2p5/home/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/n2p5/home
diff --git a/examples/gno.land/r/n2p5/home/home.gno b/examples/gno.land/r/n2p5/home/home.gno
new file mode 100644
index 00000000000..69b82e86d68
--- /dev/null
+++ b/examples/gno.land/r/n2p5/home/home.gno
@@ -0,0 +1,73 @@
+package home
+
+import (
+ "std"
+ "strings"
+
+ "gno.land/p/n2p5/chonk"
+
+ "gno.land/r/leon/hof"
+ "gno.land/r/n2p5/config"
+)
+
+var (
+ active = chonk.New()
+ preview = chonk.New()
+)
+
+func init() {
+ hof.Register()
+}
+
+// Add appends a string to the preview Chonk.
+func Add(chunk string) {
+ assertAdmin()
+ preview.Add(chunk)
+}
+
+// Flush clears the preview Chonk.
+func Flush() {
+ assertAdmin()
+ preview.Flush()
+}
+
+// Promote promotes the preview Chonk to the active Chonk
+// and creates a new preview Chonk.
+func Promote() {
+ assertAdmin()
+ active = preview
+ preview = chonk.New()
+}
+
+// Render returns the contents of the scanner for the active or preview Chonk
+// based on the path provided.
+func Render(path string) string {
+ var result string
+ scanner := getScanner(path)
+ for scanner.Scan() {
+ result += scanner.Text()
+ }
+ return result
+}
+
+// assertAdmin panics if the caller is not an admin as defined in the config realm.
+func assertAdmin() {
+ caller := std.PrevRealm().Addr()
+ if !config.IsAdmin(caller) {
+ panic("forbidden: must be admin")
+ }
+}
+
+// getScanner returns the scanner for the active or preview Chonk based
+// on the path provided.
+func getScanner(path string) *chonk.Scanner {
+ if isPreview(path) {
+ return preview.Scanner()
+ }
+ return active.Scanner()
+}
+
+// isPreview returns true if the path prefix is "preview".
+func isPreview(path string) bool {
+ return strings.HasPrefix(path, "preview")
+}
diff --git a/examples/gno.land/r/n2p5/loci/gno.mod b/examples/gno.land/r/n2p5/loci/gno.mod
new file mode 100644
index 00000000000..131e0d73467
--- /dev/null
+++ b/examples/gno.land/r/n2p5/loci/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/n2p5/loci
diff --git a/examples/gno.land/r/n2p5/loci/loci.gno b/examples/gno.land/r/n2p5/loci/loci.gno
new file mode 100644
index 00000000000..36f282e729f
--- /dev/null
+++ b/examples/gno.land/r/n2p5/loci/loci.gno
@@ -0,0 +1,68 @@
+package loci
+
+import (
+ "encoding/base64"
+ "std"
+
+ "gno.land/p/demo/ufmt"
+ "gno.land/p/n2p5/loci"
+)
+
+var store *loci.LociStore
+
+func init() {
+ store = loci.New()
+}
+
+// Set takes a base64 encoded string and stores it in the Loci store.
+// Keyed by the address of the caller. It also emits a "set" event with
+// the address of the caller.
+func Set(value string) {
+ b, err := base64.StdEncoding.DecodeString(value)
+ if err != nil {
+ panic(err)
+ }
+ store.Set(b)
+ std.Emit("SetValue", "ForAddr", string(std.PrevRealm().Addr()))
+}
+
+// Get retrieves the value stored at the provided address and
+// returns it as a base64 encoded string.
+func Get(addr std.Address) string {
+ return base64.StdEncoding.EncodeToString(store.Get(addr))
+}
+
+func Render(path string) string {
+ if path == "" {
+ return about
+ }
+ return renderGet(std.Address(path))
+}
+
+func renderGet(addr std.Address) string {
+ value := "```\n" + Get(addr) + "\n```"
+
+ return ufmt.Sprintf(`
+# Loci Value Viewer
+
+**Address:** %s
+
+%s
+
+`, addr, value)
+}
+
+const about = `
+# Welcome to Loci
+
+Loci is a simple key-value store keyed by the caller's gno.land address.
+Only the caller can set the value for their address, but anyone can
+retrieve the value for any address. There are only two functions: Set and Get.
+If you'd like to set a value, simply base64 encode any message you'd like and
+it will be stored in in Loci. If you'd like to retrieve a value, simply provide
+the address of the value you'd like to retrieve.
+
+For convenience, you can also use gnoweb to view the value for a given address,
+if one exists. For instance append :g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t to
+this URL to view the value stored at that address.
+`
diff --git a/examples/gno.land/r/nemanya/config/config.gno b/examples/gno.land/r/nemanya/config/config.gno
new file mode 100644
index 00000000000..795e48c94c1
--- /dev/null
+++ b/examples/gno.land/r/nemanya/config/config.gno
@@ -0,0 +1,63 @@
+package config
+
+import (
+ "errors"
+ "std"
+)
+
+var (
+ main std.Address
+ backup std.Address
+
+ ErrInvalidAddr = errors.New("Invalid address")
+ ErrUnauthorized = errors.New("Unauthorized")
+)
+
+func init() {
+ main = "g1x9qyf6f34v2g52k4q5smn5tctmj3hl2kj7l2ql"
+}
+
+func Address() std.Address {
+ return main
+}
+
+func Backup() std.Address {
+ return backup
+}
+
+func SetAddress(a std.Address) error {
+ if !a.IsValid() {
+ return ErrInvalidAddr
+ }
+
+ if err := checkAuthorized(); err != nil {
+ return err
+ }
+
+ main = a
+ return nil
+}
+
+func SetBackup(a std.Address) error {
+ if !a.IsValid() {
+ return ErrInvalidAddr
+ }
+
+ if err := checkAuthorized(); err != nil {
+ return err
+ }
+
+ backup = a
+ return nil
+}
+
+func checkAuthorized() error {
+ caller := std.PrevRealm().Addr()
+ isAuthorized := caller == main || caller == backup
+
+ if !isAuthorized {
+ return ErrUnauthorized
+ }
+
+ return nil
+}
diff --git a/examples/gno.land/r/nemanya/config/gno.mod b/examples/gno.land/r/nemanya/config/gno.mod
new file mode 100644
index 00000000000..4388b5bd525
--- /dev/null
+++ b/examples/gno.land/r/nemanya/config/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/nemanya/config
diff --git a/examples/gno.land/r/nemanya/home/gno.mod b/examples/gno.land/r/nemanya/home/gno.mod
new file mode 100644
index 00000000000..d0220197489
--- /dev/null
+++ b/examples/gno.land/r/nemanya/home/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/nemanya/home
diff --git a/examples/gno.land/r/nemanya/home/home.gno b/examples/gno.land/r/nemanya/home/home.gno
new file mode 100644
index 00000000000..08e24baecfd
--- /dev/null
+++ b/examples/gno.land/r/nemanya/home/home.gno
@@ -0,0 +1,280 @@
+package home
+
+import (
+ "std"
+ "strings"
+
+ "gno.land/p/demo/ufmt"
+ "gno.land/r/nemanya/config"
+)
+
+type SocialLink struct {
+ URL string
+ Text string
+}
+
+type Sponsor struct {
+ Address std.Address
+ Amount std.Coins
+}
+
+type Project struct {
+ Name string
+ Description string
+ URL string
+ ImageURL string
+ Sponsors map[std.Address]Sponsor
+}
+
+var (
+ textArt string
+ aboutMe string
+ sponsorInfo string
+ socialLinks map[string]SocialLink
+ gnoProjects map[string]Project
+ otherProjects map[string]Project
+ totalDonations std.Coins
+)
+
+func init() {
+ textArt = renderTextArt()
+ aboutMe = "I am a student of IT at Faculty of Sciences in Novi Sad, Serbia. My background is mainly in web and low-level programming, but since Web3 Bootcamp at Petnica this year I've been actively learning about blockchain and adjacent technologies. I am excited about contributing to the gno.land ecosystem and learning from the community.\n\n"
+ sponsorInfo = "You can sponsor a project by sending GNOT to this address. Your sponsorship will be displayed on the project page. Thank you for supporting the development of gno.land!\n\n"
+
+ socialLinks = map[string]SocialLink{
+ "GitHub": {URL: "https://github.com/Nemanya8", Text: "Explore my repositories and open-source contributions."},
+ "LinkedIn": {URL: "https://www.linkedin.com/in/nemanjamatic/", Text: "Connect with me professionally."},
+ "Email Me": {URL: "mailto:matic.nemanya@gmail.com", Text: "Reach out for collaboration or inquiries."},
+ }
+
+ gnoProjects = make(map[string]Project)
+ otherProjects = make(map[string]Project)
+
+ gnoProjects["Liberty Bridge"] = Project{
+ Name: "Liberty Bridge",
+ Description: "Liberty Bridge was my first Web3 project, developed as part of the Web3 Bootcamp at Petnica. This project served as a centralized bridge between Ethereum and gno.land, enabling seamless asset transfers and fostering interoperability between the two ecosystems.\n\n The primary objective of Liberty Bridge was to address the challenges of connecting decentralized networks by implementing a user-friendly solution that simplified the process for users. The project incorporated mechanisms to securely transfer assets between the Ethereum and gno.land blockchains, ensuring efficiency and reliability while maintaining a centralized framework for governance and operations.\n\n Through this project, I gained hands-on knowledge of blockchain interoperability, Web3 protocols, and the intricacies of building solutions that bridge different blockchain ecosystems.\n\n",
+ URL: "https://gno.land",
+ ImageURL: "https://github.com/Milosevic02/LibertyBridge/raw/main/lb_banner.png",
+ Sponsors: make(map[std.Address]Sponsor),
+ }
+
+ otherProjects["Incognito"] = Project{
+ Name: "Incognito",
+ Description: "Incognito is a Web3 platform built for Ethereum-based chains, designed to connect advertisers with users in a privacy-first and mutually beneficial way. Its modular architecture makes it easily expandable to other blockchains. Developed during the ETH Sofia Hackathon, it was recognized as a winning project for its innovation and impact.\n\n The platform allows advertisers to send personalized ads while sharing a portion of the marketing budget with users. It uses machine learning to match users based on wallet activity, ensuring precise targeting. User emails are stored securely on-chain and never shared, prioritizing privacy and transparency.\n\n With all campaign data stored on-chain, Incognito ensures decentralization and accountability. By rewarding users and empowering advertisers, it sets a new standard for fair and transparent blockchain-based advertising.",
+ URL: "https://github.com/Milosevic02/Incognito-ETHSofia",
+ ImageURL: "",
+ Sponsors: make(map[std.Address]Sponsor),
+ }
+}
+
+func Render(path string) string {
+ var sb strings.Builder
+ sb.WriteString("# Hi, I'm\n")
+ sb.WriteString(textArt)
+ sb.WriteString("---\n")
+ sb.WriteString("## About me\n")
+ sb.WriteString(aboutMe)
+ sb.WriteString(sponsorInfo)
+ sb.WriteString(ufmt.Sprintf("# Total Sponsor Donations: %s\n", totalDonations.String()))
+ sb.WriteString("---\n")
+ sb.WriteString(renderProjects(gnoProjects, "Gno Projects"))
+ sb.WriteString("---\n")
+ sb.WriteString(renderProjects(otherProjects, "Other Projects"))
+ sb.WriteString("---\n")
+ sb.WriteString(renderSocialLinks())
+
+ return sb.String()
+}
+
+func renderTextArt() string {
+ var sb strings.Builder
+ sb.WriteString("```\n")
+ sb.WriteString(" ___ ___ ___ ___ ___ ___ ___ \n")
+ sb.WriteString(" /\\__\\ /\\ \\ /\\__\\ /\\ \\ /\\__\\ |\\__\\ /\\ \\ \n")
+ sb.WriteString(" /::| | /::\\ \\ /::| | /::\\ \\ /::| | |:| | /::\\ \\ \n")
+ sb.WriteString(" /:|:| | /:/\\:\\ \\ /:|:| | /:/\\:\\ \\ /:|:| | |:| | /:/\\:\\ \\ \n")
+ sb.WriteString(" /:/|:| |__ /::\\~\\:\\ \\ /:/|:|__|__ /::\\~\\:\\ \\ /:/|:| |__ |:|__|__ /::\\~\\:\\ \\ \n")
+ sb.WriteString(" /:/ |:| /\\__\\ /:/\\:\\ \\:\\__\\ /:/ |::::\\__\\ /:/\\:\\ \\:\\__\\ /:/ |:| /\\__\\ /::::\\__\\ /:/\\:\\ \\:\\__\\\n")
+ sb.WriteString(" \\/__|:|/:/ / \\:\\~\\:\\ \\/__/ \\/__/~~/:/ / \\/__\\:\\/:/ / \\/__|:|/:/ / /:/~~/~ \\/__\\:\\/:/ / \n")
+ sb.WriteString(" |:/:/ / \\:\\ \\:\\__\\ /:/ / \\::/ / |:/:/ / /:/ / \\::/ / \n")
+ sb.WriteString(" |::/ / \\:\\ \\/__/ /:/ / /:/ / |::/ / \\/__/ /:/ / \n")
+ sb.WriteString(" /:/ / \\:\\__\\ /:/ / /:/ / /:/ / /:/ / \n")
+ sb.WriteString(" \\/__/ \\/__/ \\/__/ \\/__/ \\/__/ \\/__/ \n")
+ sb.WriteString("\n```\n")
+ return sb.String()
+}
+
+func renderSocialLinks() string {
+ var sb strings.Builder
+ sb.WriteString("## Links\n\n")
+ sb.WriteString("You can find me here:\n\n")
+ sb.WriteString(ufmt.Sprintf("- [GitHub](%s) - %s\n", socialLinks["GitHub"].URL, socialLinks["GitHub"].Text))
+ sb.WriteString(ufmt.Sprintf("- [LinkedIn](%s) - %s\n", socialLinks["LinkedIn"].URL, socialLinks["LinkedIn"].Text))
+ sb.WriteString(ufmt.Sprintf("- [Email Me](%s) - %s\n", socialLinks["Email Me"].URL, socialLinks["Email Me"].Text))
+ sb.WriteString("\n")
+ return sb.String()
+}
+
+func renderProjects(projectsMap map[string]Project, title string) string {
+ var sb strings.Builder
+ sb.WriteString(ufmt.Sprintf("## %s\n\n", title))
+ for _, project := range projectsMap {
+ if project.ImageURL != "" {
+ sb.WriteString(ufmt.Sprintf("![%s](%s)\n\n", project.Name, project.ImageURL))
+ }
+ sb.WriteString(ufmt.Sprintf("### [%s](%s)\n\n", project.Name, project.URL))
+ sb.WriteString(project.Description + "\n\n")
+
+ if len(project.Sponsors) > 0 {
+ sb.WriteString(ufmt.Sprintf("#### %s Sponsors\n", project.Name))
+ for _, sponsor := range project.Sponsors {
+ sb.WriteString(ufmt.Sprintf("- %s: %s\n", sponsor.Address.String(), sponsor.Amount.String()))
+ }
+ sb.WriteString("\n")
+ }
+ }
+ return sb.String()
+}
+
+func UpdateLink(name, newURL string) {
+ if !isAuthorized(std.PrevRealm().Addr()) {
+ panic(config.ErrUnauthorized)
+ }
+
+ if _, exists := socialLinks[name]; !exists {
+ panic("Link with the given name does not exist")
+ }
+
+ socialLinks[name] = SocialLink{
+ URL: newURL,
+ Text: socialLinks[name].Text,
+ }
+}
+
+func UpdateAboutMe(text string) {
+ if !isAuthorized(std.PrevRealm().Addr()) {
+ panic(config.ErrUnauthorized)
+ }
+
+ aboutMe = text
+}
+
+func AddGnoProject(name, description, url, imageURL string) {
+ if !isAuthorized(std.PrevRealm().Addr()) {
+ panic(config.ErrUnauthorized)
+ }
+ project := Project{
+ Name: name,
+ Description: description,
+ URL: url,
+ ImageURL: imageURL,
+ Sponsors: make(map[std.Address]Sponsor),
+ }
+ gnoProjects[name] = project
+}
+
+func DeleteGnoProject(projectName string) {
+ if !isAuthorized(std.PrevRealm().Addr()) {
+ panic(config.ErrUnauthorized)
+ }
+
+ if _, exists := gnoProjects[projectName]; !exists {
+ panic("Project not found")
+ }
+
+ delete(gnoProjects, projectName)
+}
+
+func AddOtherProject(name, description, url, imageURL string) {
+ if !isAuthorized(std.PrevRealm().Addr()) {
+ panic(config.ErrUnauthorized)
+ }
+ project := Project{
+ Name: name,
+ Description: description,
+ URL: url,
+ ImageURL: imageURL,
+ Sponsors: make(map[std.Address]Sponsor),
+ }
+ otherProjects[name] = project
+}
+
+func RemoveOtherProject(projectName string) {
+ if !isAuthorized(std.PrevRealm().Addr()) {
+ panic(config.ErrUnauthorized)
+ }
+
+ if _, exists := otherProjects[projectName]; !exists {
+ panic("Project not found")
+ }
+
+ delete(otherProjects, projectName)
+}
+
+func isAuthorized(addr std.Address) bool {
+ return addr == config.Address() || addr == config.Backup()
+}
+
+func SponsorGnoProject(projectName string) {
+ address := std.GetOrigCaller()
+ amount := std.GetOrigSend()
+
+ if amount.AmountOf("ugnot") == 0 {
+ panic("Donation must include GNOT")
+ }
+
+ project, exists := gnoProjects[projectName]
+ if !exists {
+ panic("Gno project not found")
+ }
+
+ project.Sponsors[address] = Sponsor{
+ Address: address,
+ Amount: project.Sponsors[address].Amount.Add(amount),
+ }
+
+ totalDonations = totalDonations.Add(amount)
+
+ gnoProjects[projectName] = project
+}
+
+func SponsorOtherProject(projectName string) {
+ address := std.GetOrigCaller()
+ amount := std.GetOrigSend()
+
+ if amount.AmountOf("ugnot") == 0 {
+ panic("Donation must include GNOT")
+ }
+
+ project, exists := otherProjects[projectName]
+ if !exists {
+ panic("Other project not found")
+ }
+
+ project.Sponsors[address] = Sponsor{
+ Address: address,
+ Amount: project.Sponsors[address].Amount.Add(amount),
+ }
+
+ totalDonations = totalDonations.Add(amount)
+
+ otherProjects[projectName] = project
+}
+
+func Withdraw() string {
+ if !isAuthorized(std.PrevRealm().Addr()) {
+ panic(config.ErrUnauthorized)
+ }
+
+ banker := std.GetBanker(std.BankerTypeRealmSend)
+ realmAddress := std.GetOrigPkgAddr()
+ coins := banker.GetCoins(realmAddress)
+
+ if len(coins) == 0 {
+ return "No coins available to withdraw"
+ }
+
+ banker.SendCoins(realmAddress, config.Address(), coins)
+
+ return "Successfully withdrew all coins to config address"
+}
diff --git a/examples/gno.land/r/stefann/home/gno.mod b/examples/gno.land/r/stefann/home/gno.mod
new file mode 100644
index 00000000000..89071aa70fb
--- /dev/null
+++ b/examples/gno.land/r/stefann/home/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/stefann/home
diff --git a/examples/gno.land/r/stefann/home/home.gno b/examples/gno.land/r/stefann/home/home.gno
new file mode 100644
index 00000000000..f54721ce37c
--- /dev/null
+++ b/examples/gno.land/r/stefann/home/home.gno
@@ -0,0 +1,288 @@
+package home
+
+import (
+ "sort"
+ "std"
+ "strings"
+
+ "gno.land/p/demo/avl"
+ "gno.land/p/demo/ownable"
+ "gno.land/p/demo/ufmt"
+ "gno.land/r/demo/users"
+ "gno.land/r/leon/hof"
+
+ "gno.land/r/stefann/registry"
+)
+
+type City struct {
+ Name string
+ URL string
+}
+
+type Sponsor struct {
+ Address std.Address
+ Amount std.Coins
+}
+
+type Profile struct {
+ aboutMe []string
+}
+
+type Travel struct {
+ cities []City
+ currentCityIndex int
+ jarLink string
+}
+
+type Sponsorship struct {
+ maxSponsors int
+ sponsors *avl.Tree
+ DonationsCount int
+ sponsorsCount int
+}
+
+var (
+ profile Profile
+ travel Travel
+ sponsorship Sponsorship
+ owner *ownable.Ownable
+)
+
+func init() {
+ owner = ownable.NewWithAddress(registry.MainAddr())
+ hof.Register()
+
+ profile = Profile{
+ aboutMe: []string{
+ `## About Me`,
+ `### Hey there! I’m Stefan, a student of Computer Science. I’m all about exploring and adventure — whether it’s diving into the latest tech or discovering a new city, I’m always up for the challenge!`,
+
+ `## Contributions`,
+ `### I'm just getting started, but you can follow my journey through gno.land right [here](https://github.com/gnolang/hackerspace/issues/94) 🔗`,
+ },
+ }
+
+ travel = Travel{
+ cities: []City{
+ {Name: "Venice", URL: "https://i.ibb.co/1mcZ7b1/venice.jpg"},
+ {Name: "Tokyo", URL: "https://i.ibb.co/wNDJv3H/tokyo.jpg"},
+ {Name: "São Paulo", URL: "https://i.ibb.co/yWMq2Sn/sao-paulo.jpg"},
+ {Name: "Toronto", URL: "https://i.ibb.co/pb95HJB/toronto.jpg"},
+ {Name: "Bangkok", URL: "https://i.ibb.co/pQy3w2g/bangkok.jpg"},
+ {Name: "New York", URL: "https://i.ibb.co/6JWLm0h/new-york.jpg"},
+ {Name: "Paris", URL: "https://i.ibb.co/q9vf6Hs/paris.jpg"},
+ {Name: "Kandersteg", URL: "https://i.ibb.co/60DzywD/kandersteg.jpg"},
+ {Name: "Rothenburg", URL: "https://i.ibb.co/cr8d2rQ/rothenburg.jpg"},
+ {Name: "Capetown", URL: "https://i.ibb.co/bPGn0v3/capetown.jpg"},
+ {Name: "Sydney", URL: "https://i.ibb.co/TBNzqfy/sydney.jpg"},
+ {Name: "Oeschinen Lake", URL: "https://i.ibb.co/QJQwp2y/oeschinen-lake.jpg"},
+ {Name: "Barra Grande", URL: "https://i.ibb.co/z4RXKc1/barra-grande.jpg"},
+ {Name: "London", URL: "https://i.ibb.co/CPGtvgr/london.jpg"},
+ },
+ currentCityIndex: 0,
+ jarLink: "https://TODO", // This value should be injected through UpdateJarLink after deployment.
+ }
+
+ sponsorship = Sponsorship{
+ maxSponsors: 3,
+ sponsors: avl.NewTree(),
+ DonationsCount: 0,
+ sponsorsCount: 0,
+ }
+}
+
+func UpdateCities(newCities []City) {
+ owner.AssertCallerIsOwner()
+ travel.cities = newCities
+}
+
+func AddCities(newCities ...City) {
+ owner.AssertCallerIsOwner()
+
+ travel.cities = append(travel.cities, newCities...)
+}
+
+func UpdateJarLink(newLink string) {
+ owner.AssertCallerIsOwner()
+ travel.jarLink = newLink
+}
+
+func UpdateAboutMe(aboutMeStr string) {
+ owner.AssertCallerIsOwner()
+ profile.aboutMe = strings.Split(aboutMeStr, "|")
+}
+
+func AddAboutMeRows(newRows ...string) {
+ owner.AssertCallerIsOwner()
+
+ profile.aboutMe = append(profile.aboutMe, newRows...)
+}
+
+func UpdateMaxSponsors(newMax int) {
+ owner.AssertCallerIsOwner()
+ if newMax <= 0 {
+ panic("maxSponsors must be greater than zero")
+ }
+ sponsorship.maxSponsors = newMax
+}
+
+func Donate() {
+ address := std.GetOrigCaller()
+ amount := std.GetOrigSend()
+
+ if amount.AmountOf("ugnot") == 0 {
+ panic("Donation must include GNOT")
+ }
+
+ existingAmount, exists := sponsorship.sponsors.Get(address.String())
+ if exists {
+ updatedAmount := existingAmount.(std.Coins).Add(amount)
+ sponsorship.sponsors.Set(address.String(), updatedAmount)
+ } else {
+ sponsorship.sponsors.Set(address.String(), amount)
+ sponsorship.sponsorsCount++
+ }
+
+ travel.currentCityIndex++
+ sponsorship.DonationsCount++
+
+ banker := std.GetBanker(std.BankerTypeRealmSend)
+ ownerAddr := registry.MainAddr()
+ banker.SendCoins(std.CurrentRealm().Addr(), ownerAddr, banker.GetCoins(std.CurrentRealm().Addr()))
+}
+
+type SponsorSlice []Sponsor
+
+func (s SponsorSlice) Len() int {
+ return len(s)
+}
+
+func (s SponsorSlice) Less(i, j int) bool {
+ return s[i].Amount.AmountOf("ugnot") > s[j].Amount.AmountOf("ugnot")
+}
+
+func (s SponsorSlice) Swap(i, j int) {
+ s[i], s[j] = s[j], s[i]
+}
+
+func GetTopSponsors() []Sponsor {
+ var sponsorSlice SponsorSlice
+
+ sponsorship.sponsors.Iterate("", "", func(key string, value interface{}) bool {
+ addr := std.Address(key)
+ amount := value.(std.Coins)
+ sponsorSlice = append(sponsorSlice, Sponsor{Address: addr, Amount: amount})
+ return false
+ })
+
+ sort.Sort(sponsorSlice)
+ return sponsorSlice
+}
+
+func GetTotalDonations() int {
+ total := 0
+ sponsorship.sponsors.Iterate("", "", func(key string, value interface{}) bool {
+ total += int(value.(std.Coins).AmountOf("ugnot"))
+ return false
+ })
+ return total
+}
+
+func Render(path string) string {
+ out := ufmt.Sprintf("# Exploring %s!\n\n", travel.cities[travel.currentCityIndex].Name)
+
+ out += renderAboutMe()
+ out += "\n\n"
+ out += renderTips()
+
+ return out
+}
+
+func renderAboutMe() string {
+ out := ""
+
+ out += ufmt.Sprintf("![Current Location](%s)\n\n", travel.cities[travel.currentCityIndex%len(travel.cities)].URL)
+
+ for _, rows := range profile.aboutMe {
+ out += rows + "\n\n"
+ }
+
+ return out
+}
+
+func renderTips() string {
+ out := "# Help Me Travel The World\n\n"
+
+ out += ufmt.Sprintf("## I am currently in %s, tip the jar to send me somewhere else!\n\n", travel.cities[travel.currentCityIndex].Name)
+ out += "### **Click** the jar, **tip** in GNOT coins, and **watch** my background change as I head to a new adventure!\n\n"
+
+ out += renderTipsJar() + "\n\n"
+
+ out += renderSponsors()
+
+ return out
+}
+
+func formatAddress(address string) string {
+ if len(address) <= 8 {
+ return address
+ }
+ return address[:4] + "..." + address[len(address)-4:]
+}
+
+func getDisplayName(addr std.Address) string {
+ if user := users.GetUserByAddress(addr); user != nil {
+ return user.Name
+ }
+ return formatAddress(addr.String())
+}
+
+func formatAmount(amount std.Coins) string {
+ ugnot := amount.AmountOf("ugnot")
+ if ugnot >= 1000000 {
+ gnot := float64(ugnot) / 1000000
+ return ufmt.Sprintf("`%v`*GNOT*", gnot)
+ }
+ return ufmt.Sprintf("`%d`*ugnot*", ugnot)
+}
+
+func renderSponsors() string {
+ out := "## Sponsor Leaderboard\n\n"
+
+ if sponsorship.sponsorsCount == 0 {
+ return out + "No sponsors yet. Be the first to tip the jar!\n"
+ }
+
+ topSponsors := GetTopSponsors()
+ numSponsors := len(topSponsors)
+ if numSponsors > sponsorship.maxSponsors {
+ numSponsors = sponsorship.maxSponsors
+ }
+
+ for i := 0; i < numSponsors; i++ {
+ sponsor := topSponsors[i]
+ position := ""
+ switch i {
+ case 0:
+ position = "🥇"
+ case 1:
+ position = "🥈"
+ case 2:
+ position = "🥉"
+ default:
+ position = ufmt.Sprintf("%d.", i+1)
+ }
+
+ out += ufmt.Sprintf("%s **%s** - %s\n\n",
+ position,
+ getDisplayName(sponsor.Address),
+ formatAmount(sponsor.Amount),
+ )
+ }
+
+ return out + "\n"
+}
+
+func renderTipsJar() string {
+ return ufmt.Sprintf("[![Tips Jar](https://i.ibb.co/4TH9zbw/tips-jar.png)](%s)", travel.jarLink)
+}
diff --git a/examples/gno.land/r/stefann/home/home_test.gno b/examples/gno.land/r/stefann/home/home_test.gno
new file mode 100644
index 00000000000..b8ea88670a6
--- /dev/null
+++ b/examples/gno.land/r/stefann/home/home_test.gno
@@ -0,0 +1,278 @@
+package home
+
+import (
+ "std"
+ "strings"
+ "testing"
+
+ "gno.land/p/demo/avl"
+ "gno.land/p/demo/testutils"
+)
+
+func TestUpdateAboutMe(t *testing.T) {
+ var owner = std.Address("g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8")
+ std.TestSetOrigCaller(owner)
+
+ profile.aboutMe = []string{}
+
+ UpdateAboutMe("This is my new bio.|I love coding!")
+
+ expected := []string{"This is my new bio.", "I love coding!"}
+
+ if len(profile.aboutMe) != len(expected) {
+ t.Fatalf("expected aboutMe to have length %d, got %d", len(expected), len(profile.aboutMe))
+ }
+
+ for i := range profile.aboutMe {
+ if profile.aboutMe[i] != expected[i] {
+ t.Fatalf("expected aboutMe[%d] to be %s, got %s", i, expected[i], profile.aboutMe[i])
+ }
+ }
+}
+
+func TestUpdateCities(t *testing.T) {
+ var owner = std.Address("g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8")
+ std.TestSetOrigCaller(owner)
+
+ travel.cities = []City{}
+
+ newCities := []City{
+ {Name: "Berlin", URL: "https://example.com/berlin.jpg"},
+ {Name: "Vienna", URL: "https://example.com/vienna.jpg"},
+ }
+
+ UpdateCities(newCities)
+
+ if len(travel.cities) != 2 {
+ t.Fatalf("expected 2 cities, got %d", len(travel.cities))
+ }
+
+ if travel.cities[0].Name != "Berlin" || travel.cities[1].Name != "Vienna" {
+ t.Fatalf("expected cities to be updated to Berlin and Vienna, got %+v", travel.cities)
+ }
+}
+
+func TestUpdateJarLink(t *testing.T) {
+ var owner = std.Address("g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8")
+ std.TestSetOrigCaller(owner)
+
+ travel.jarLink = ""
+
+ UpdateJarLink("https://example.com/jar")
+
+ if travel.jarLink != "https://example.com/jar" {
+ t.Fatalf("expected jarLink to be https://example.com/jar, got %s", travel.jarLink)
+ }
+}
+
+func TestUpdateMaxSponsors(t *testing.T) {
+ var owner = std.Address("g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8")
+ std.TestSetOrigCaller(owner)
+
+ sponsorship.maxSponsors = 0
+
+ UpdateMaxSponsors(10)
+
+ if sponsorship.maxSponsors != 10 {
+ t.Fatalf("expected maxSponsors to be 10, got %d", sponsorship.maxSponsors)
+ }
+
+ defer func() {
+ if r := recover(); r == nil {
+ t.Fatalf("expected panic for setting maxSponsors to 0")
+ }
+ }()
+ UpdateMaxSponsors(0)
+}
+
+func TestAddCities(t *testing.T) {
+ var owner = std.Address("g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8")
+ std.TestSetOrigCaller(owner)
+
+ travel.cities = []City{}
+
+ AddCities(City{Name: "Berlin", URL: "https://example.com/berlin.jpg"})
+
+ if len(travel.cities) != 1 {
+ t.Fatalf("expected 1 city, got %d", len(travel.cities))
+ }
+ if travel.cities[0].Name != "Berlin" || travel.cities[0].URL != "https://example.com/berlin.jpg" {
+ t.Fatalf("expected city to be Berlin, got %+v", travel.cities[0])
+ }
+
+ AddCities(
+ City{Name: "Paris", URL: "https://example.com/paris.jpg"},
+ City{Name: "Tokyo", URL: "https://example.com/tokyo.jpg"},
+ )
+
+ if len(travel.cities) != 3 {
+ t.Fatalf("expected 3 cities, got %d", len(travel.cities))
+ }
+ if travel.cities[1].Name != "Paris" || travel.cities[2].Name != "Tokyo" {
+ t.Fatalf("expected cities to be Paris and Tokyo, got %+v", travel.cities[1:])
+ }
+}
+
+func TestAddAboutMeRows(t *testing.T) {
+ var owner = std.Address("g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8")
+ std.TestSetOrigCaller(owner)
+
+ profile.aboutMe = []string{}
+
+ AddAboutMeRows("I love exploring new technologies!")
+
+ if len(profile.aboutMe) != 1 {
+ t.Fatalf("expected 1 aboutMe row, got %d", len(profile.aboutMe))
+ }
+ if profile.aboutMe[0] != "I love exploring new technologies!" {
+ t.Fatalf("expected first aboutMe row to be 'I love exploring new technologies!', got %s", profile.aboutMe[0])
+ }
+
+ AddAboutMeRows("Travel is my passion!", "Always learning.")
+
+ if len(profile.aboutMe) != 3 {
+ t.Fatalf("expected 3 aboutMe rows, got %d", len(profile.aboutMe))
+ }
+ if profile.aboutMe[1] != "Travel is my passion!" || profile.aboutMe[2] != "Always learning." {
+ t.Fatalf("expected aboutMe rows to be 'Travel is my passion!' and 'Always learning.', got %+v", profile.aboutMe[1:])
+ }
+}
+
+func TestDonate(t *testing.T) {
+ var user = testutils.TestAddress("user")
+ std.TestSetOrigCaller(user)
+
+ sponsorship.sponsors = avl.NewTree()
+ sponsorship.DonationsCount = 0
+ sponsorship.sponsorsCount = 0
+ travel.currentCityIndex = 0
+
+ coinsSent := std.NewCoins(std.NewCoin("ugnot", 500))
+ std.TestSetOrigSend(coinsSent, std.NewCoins())
+ Donate()
+
+ existingAmount, exists := sponsorship.sponsors.Get(string(user))
+ if !exists {
+ t.Fatalf("expected sponsor to be added, but it was not found")
+ }
+
+ if existingAmount.(std.Coins).AmountOf("ugnot") != 500 {
+ t.Fatalf("expected donation amount to be 500ugnot, got %d", existingAmount.(std.Coins).AmountOf("ugnot"))
+ }
+
+ if sponsorship.DonationsCount != 1 {
+ t.Fatalf("expected DonationsCount to be 1, got %d", sponsorship.DonationsCount)
+ }
+
+ if sponsorship.sponsorsCount != 1 {
+ t.Fatalf("expected sponsorsCount to be 1, got %d", sponsorship.sponsorsCount)
+ }
+
+ if travel.currentCityIndex != 1 {
+ t.Fatalf("expected currentCityIndex to be 1, got %d", travel.currentCityIndex)
+ }
+
+ coinsSent = std.NewCoins(std.NewCoin("ugnot", 300))
+ std.TestSetOrigSend(coinsSent, std.NewCoins())
+ Donate()
+
+ existingAmount, exists = sponsorship.sponsors.Get(string(user))
+ if !exists {
+ t.Fatalf("expected sponsor to exist after second donation, but it was not found")
+ }
+
+ if existingAmount.(std.Coins).AmountOf("ugnot") != 800 {
+ t.Fatalf("expected total donation amount to be 800ugnot, got %d", existingAmount.(std.Coins).AmountOf("ugnot"))
+ }
+
+ if sponsorship.DonationsCount != 2 {
+ t.Fatalf("expected DonationsCount to be 2 after second donation, got %d", sponsorship.DonationsCount)
+ }
+
+ if travel.currentCityIndex != 2 {
+ t.Fatalf("expected currentCityIndex to be 2 after second donation, got %d", travel.currentCityIndex)
+ }
+}
+
+func TestGetTopSponsors(t *testing.T) {
+ var user = testutils.TestAddress("user")
+ std.TestSetOrigCaller(user)
+
+ sponsorship.sponsors = avl.NewTree()
+ sponsorship.sponsorsCount = 0
+
+ sponsorship.sponsors.Set("g1address1", std.NewCoins(std.NewCoin("ugnot", 300)))
+ sponsorship.sponsors.Set("g1address2", std.NewCoins(std.NewCoin("ugnot", 500)))
+ sponsorship.sponsors.Set("g1address3", std.NewCoins(std.NewCoin("ugnot", 200)))
+ sponsorship.sponsorsCount = 3
+
+ topSponsors := GetTopSponsors()
+
+ if len(topSponsors) != 3 {
+ t.Fatalf("expected 3 sponsors, got %d", len(topSponsors))
+ }
+
+ if topSponsors[0].Address.String() != "g1address2" || topSponsors[0].Amount.AmountOf("ugnot") != 500 {
+ t.Fatalf("expected top sponsor to be g1address2 with 500ugnot, got %s with %dugnot", topSponsors[0].Address.String(), topSponsors[0].Amount.AmountOf("ugnot"))
+ }
+
+ if topSponsors[1].Address.String() != "g1address1" || topSponsors[1].Amount.AmountOf("ugnot") != 300 {
+ t.Fatalf("expected second sponsor to be g1address1 with 300ugnot, got %s with %dugnot", topSponsors[1].Address.String(), topSponsors[1].Amount.AmountOf("ugnot"))
+ }
+
+ if topSponsors[2].Address.String() != "g1address3" || topSponsors[2].Amount.AmountOf("ugnot") != 200 {
+ t.Fatalf("expected third sponsor to be g1address3 with 200ugnot, got %s with %dugnot", topSponsors[2].Address.String(), topSponsors[2].Amount.AmountOf("ugnot"))
+ }
+}
+
+func TestGetTotalDonations(t *testing.T) {
+ var user = testutils.TestAddress("user")
+ std.TestSetOrigCaller(user)
+
+ sponsorship.sponsors = avl.NewTree()
+ sponsorship.sponsorsCount = 0
+
+ sponsorship.sponsors.Set("g1address1", std.NewCoins(std.NewCoin("ugnot", 300)))
+ sponsorship.sponsors.Set("g1address2", std.NewCoins(std.NewCoin("ugnot", 500)))
+ sponsorship.sponsors.Set("g1address3", std.NewCoins(std.NewCoin("ugnot", 200)))
+ sponsorship.sponsorsCount = 3
+
+ totalDonations := GetTotalDonations()
+
+ if totalDonations != 1000 {
+ t.Fatalf("expected total donations to be 1000ugnot, got %dugnot", totalDonations)
+ }
+}
+
+func TestRender(t *testing.T) {
+ travel.currentCityIndex = 0
+ travel.cities = []City{
+ {Name: "Venice", URL: "https://example.com/venice.jpg"},
+ {Name: "Paris", URL: "https://example.com/paris.jpg"},
+ }
+
+ output := Render("")
+
+ expectedCity := "Venice"
+ if !strings.Contains(output, expectedCity) {
+ t.Fatalf("expected output to contain city name '%s', got %s", expectedCity, output)
+ }
+
+ expectedURL := "https://example.com/venice.jpg"
+ if !strings.Contains(output, expectedURL) {
+ t.Fatalf("expected output to contain city URL '%s', got %s", expectedURL, output)
+ }
+
+ travel.currentCityIndex = 1
+ output = Render("")
+
+ expectedCity = "Paris"
+ if !strings.Contains(output, expectedCity) {
+ t.Fatalf("expected output to contain city name '%s', got %s", expectedCity, output)
+ }
+
+ expectedURL = "https://example.com/paris.jpg"
+ if !strings.Contains(output, expectedURL) {
+ t.Fatalf("expected output to contain city URL '%s', got %s", expectedURL, output)
+ }
+}
diff --git a/examples/gno.land/r/stefann/registry/gno.mod b/examples/gno.land/r/stefann/registry/gno.mod
new file mode 100644
index 00000000000..7ef0c32030f
--- /dev/null
+++ b/examples/gno.land/r/stefann/registry/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/stefann/registry
diff --git a/examples/gno.land/r/stefann/registry/registry.gno b/examples/gno.land/r/stefann/registry/registry.gno
new file mode 100644
index 00000000000..6f56d105e4b
--- /dev/null
+++ b/examples/gno.land/r/stefann/registry/registry.gno
@@ -0,0 +1,51 @@
+package registry
+
+import (
+ "errors"
+ "std"
+
+ "gno.land/p/demo/ownable"
+)
+
+var (
+ mainAddr std.Address
+ backupAddr std.Address
+ owner *ownable.Ownable
+)
+
+func init() {
+ mainAddr = "g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8"
+ backupAddr = "g13awn2575t8s2vf3svlprc4dg0e9z5wchejdxk8"
+
+ owner = ownable.NewWithAddress(mainAddr)
+}
+
+func MainAddr() std.Address {
+ return mainAddr
+}
+
+func BackupAddr() std.Address {
+ return backupAddr
+}
+
+func SetMainAddr(addr std.Address) error {
+ if !addr.IsValid() {
+ return errors.New("config: invalid address")
+ }
+
+ owner.AssertCallerIsOwner()
+
+ mainAddr = addr
+ return nil
+}
+
+func SetBackupAddr(addr std.Address) error {
+ if !addr.IsValid() {
+ return errors.New("config: invalid address")
+ }
+
+ owner.AssertCallerIsOwner()
+
+ backupAddr = addr
+ return nil
+}
diff --git a/examples/gno.land/r/sys/params/gno.mod b/examples/gno.land/r/sys/params/gno.mod
new file mode 100644
index 00000000000..c633412ced7
--- /dev/null
+++ b/examples/gno.land/r/sys/params/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/sys/params
diff --git a/examples/gno.land/r/sys/params/params.gno b/examples/gno.land/r/sys/params/params.gno
new file mode 100644
index 00000000000..fa04c90de3f
--- /dev/null
+++ b/examples/gno.land/r/sys/params/params.gno
@@ -0,0 +1,54 @@
+// Package params provides functions for creating parameter executors that
+// interface with the Params Keeper.
+//
+// This package enables setting various parameter types (such as strings,
+// integers, booleans, and byte slices) through the GovDAO proposal mechanism.
+// Each function returns an executor that, when called, sets the specified
+// parameter in the Params Keeper.
+//
+// The executors are designed to be used within governance proposals to modify
+// parameters dynamically. The integration with the GovDAO allows for parameter
+// changes to be proposed and executed in a controlled manner, ensuring that
+// modifications are subject to governance processes.
+//
+// Example usage:
+//
+// executor := params.NewStringPropExecutor("exampleKey", "exampleValue")
+// // This executor can be used in a governance proposal to set the parameter.
+package params
+
+import (
+ "std"
+
+ "gno.land/p/demo/dao"
+ "gno.land/r/gov/dao/bridge"
+)
+
+func NewStringPropExecutor(key string, value string) dao.Executor {
+ return newPropExecutor(key, func() { std.SetParamString(key, value) })
+}
+
+func NewInt64PropExecutor(key string, value int64) dao.Executor {
+ return newPropExecutor(key, func() { std.SetParamInt64(key, value) })
+}
+
+func NewUint64PropExecutor(key string, value uint64) dao.Executor {
+ return newPropExecutor(key, func() { std.SetParamUint64(key, value) })
+}
+
+func NewBoolPropExecutor(key string, value bool) dao.Executor {
+ return newPropExecutor(key, func() { std.SetParamBool(key, value) })
+}
+
+func NewBytesPropExecutor(key string, value []byte) dao.Executor {
+ return newPropExecutor(key, func() { std.SetParamBytes(key, value) })
+}
+
+func newPropExecutor(key string, fn func()) dao.Executor {
+ callback := func() error {
+ fn()
+ std.Emit("set", "k", key)
+ return nil
+ }
+ return bridge.GovDAO().NewGovDAOExecutor(callback)
+}
diff --git a/examples/gno.land/r/sys/params/params_test.gno b/examples/gno.land/r/sys/params/params_test.gno
new file mode 100644
index 00000000000..eaa1ad039d3
--- /dev/null
+++ b/examples/gno.land/r/sys/params/params_test.gno
@@ -0,0 +1,15 @@
+package params
+
+import "testing"
+
+// Testing this package is limited because it only contains an `std.Set` method
+// without a corresponding `std.Get` method. For comprehensive testing, refer to
+// the tests located in the r/gov/dao/ directory, specifically in one of the
+// propX_filetest.gno files.
+
+func TestNewStringPropExecutor(t *testing.T) {
+ executor := NewStringPropExecutor("foo", "bar")
+ if executor == nil {
+ t.Errorf("executor shouldn't be nil")
+ }
+}
diff --git a/examples/gno.land/r/sys/users/gno.mod b/examples/gno.land/r/sys/users/gno.mod
index 774a364a272..e5e84a49faf 100644
--- a/examples/gno.land/r/sys/users/gno.mod
+++ b/examples/gno.land/r/sys/users/gno.mod
@@ -1,6 +1 @@
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
index 852626622e4..71869fda1a1 100644
--- a/examples/gno.land/r/sys/users/verify.gno
+++ b/examples/gno.land/r/sys/users/verify.gno
@@ -7,7 +7,7 @@ import (
"gno.land/r/demo/users"
)
-const admin = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" // @moul
+const admin = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul
type VerifyNameFunc func(enabled bool, address std.Address, name string) bool
@@ -48,8 +48,8 @@ func VerifyNameByUser(enable bool, address std.Address, name string) bool {
// Enable this package.
func AdminEnable() {
- if err := owner.CallerIsOwner(); err != nil {
- panic(err)
+ if !owner.CallerIsOwner() {
+ panic(ownable.ErrUnauthorized)
}
enabled = true
@@ -57,8 +57,8 @@ func AdminEnable() {
// Disable this package.
func AdminDisable() {
- if err := owner.CallerIsOwner(); err != nil {
- panic(err)
+ if !owner.CallerIsOwner() {
+ panic(ownable.ErrUnauthorized)
}
enabled = false
@@ -66,8 +66,8 @@ func AdminDisable() {
// AdminUpdateVerifyCall updates the method that verifies the namespace.
func AdminUpdateVerifyCall(check VerifyNameFunc) {
- if err := owner.CallerIsOwner(); err != nil {
- panic(err)
+ if !owner.CallerIsOwner() {
+ panic(ownable.ErrUnauthorized)
}
checkFunc = check
@@ -75,8 +75,8 @@ func AdminUpdateVerifyCall(check VerifyNameFunc) {
// AdminTransferOwnership transfers the ownership to a new owner.
func AdminTransferOwnership(newOwner std.Address) error {
- if err := owner.CallerIsOwner(); err != nil {
- panic(err)
+ if !owner.CallerIsOwner() {
+ panic(ownable.ErrUnauthorized)
}
return owner.TransferOwnership(newOwner)
diff --git a/examples/gno.land/r/sys/validators/gno.mod b/examples/gno.land/r/sys/validators/gno.mod
deleted file mode 100644
index d9d129dd543..00000000000
--- a/examples/gno.land/r/sys/validators/gno.mod
+++ /dev/null
@@ -1,12 +0,0 @@
-module gno.land/r/sys/validators
-
-require (
- gno.land/p/demo/avl v0.0.0-latest
- gno.land/p/demo/seqid v0.0.0-latest
- gno.land/p/demo/testutils v0.0.0-latest
- gno.land/p/demo/uassert v0.0.0-latest
- gno.land/p/demo/ufmt v0.0.0-latest
- gno.land/p/gov/proposal v0.0.0-latest
- gno.land/p/nt/poa v0.0.0-latest
- gno.land/p/sys/validators v0.0.0-latest
-)
diff --git a/examples/gno.land/r/sys/validators/poc.gno b/examples/gno.land/r/sys/validators/poc.gno
deleted file mode 100644
index e088b3b4293..00000000000
--- a/examples/gno.land/r/sys/validators/poc.gno
+++ /dev/null
@@ -1,66 +0,0 @@
-package validators
-
-import (
- "std"
-
- "gno.land/p/gov/proposal"
- "gno.land/p/sys/validators"
-)
-
-const daoPkgPath = "gno.land/r/gov/dao"
-
-const (
- errNoChangesProposed = "no set changes proposed"
- errNotGovDAO = "caller not govdao executor"
-)
-
-// NewPropExecutor creates a new executor that wraps a changes closure
-// proposal. This wrapper is required to ensure the GovDAO Realm actually
-// executed the callback.
-//
-// Concept adapted from:
-// https://github.com/gnolang/gno/pull/1945
-func NewPropExecutor(changesFn func() []validators.Validator) proposal.Executor {
- if changesFn == nil {
- panic(errNoChangesProposed)
- }
-
- callback := func() error {
- // Make sure the GovDAO executor runs the valset changes
- assertGovDAOCaller()
-
- for _, change := range changesFn() {
- if change.VotingPower == 0 {
- // This change request is to remove the validator
- removeValidator(change.Address)
-
- continue
- }
-
- // This change request is to add the validator
- addValidator(change)
- }
-
- return nil
- }
-
- return proposal.NewExecutor(callback)
-}
-
-// assertGovDAOCaller verifies the caller is the GovDAO executor
-func assertGovDAOCaller() {
- if std.PrevRealm().PkgPath() != daoPkgPath {
- panic(errNotGovDAO)
- }
-}
-
-// IsValidator returns a flag indicating if the given bech32 address
-// is part of the validator set
-func IsValidator(addr std.Address) bool {
- return vp.IsValidator(addr)
-}
-
-// GetValidators returns the typed validator set
-func GetValidators() []validators.Validator {
- return vp.GetValidators()
-}
diff --git a/examples/gno.land/r/sys/validators/doc.gno b/examples/gno.land/r/sys/validators/v2/doc.gno
similarity index 100%
rename from examples/gno.land/r/sys/validators/doc.gno
rename to examples/gno.land/r/sys/validators/v2/doc.gno
diff --git a/examples/gno.land/r/sys/validators/v2/gno.mod b/examples/gno.land/r/sys/validators/v2/gno.mod
new file mode 100644
index 00000000000..beae6e95d34
--- /dev/null
+++ b/examples/gno.land/r/sys/validators/v2/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/sys/validators/v2
diff --git a/examples/gno.land/r/sys/validators/gnosdk.gno b/examples/gno.land/r/sys/validators/v2/gnosdk.gno
similarity index 100%
rename from examples/gno.land/r/sys/validators/gnosdk.gno
rename to examples/gno.land/r/sys/validators/v2/gnosdk.gno
diff --git a/examples/gno.land/r/sys/validators/init.gno b/examples/gno.land/r/sys/validators/v2/init.gno
similarity index 100%
rename from examples/gno.land/r/sys/validators/init.gno
rename to examples/gno.land/r/sys/validators/v2/init.gno
diff --git a/examples/gno.land/r/sys/validators/v2/poc.gno b/examples/gno.land/r/sys/validators/v2/poc.gno
new file mode 100644
index 00000000000..760edc39d1e
--- /dev/null
+++ b/examples/gno.land/r/sys/validators/v2/poc.gno
@@ -0,0 +1,52 @@
+package validators
+
+import (
+ "std"
+
+ "gno.land/p/demo/dao"
+ "gno.land/p/sys/validators"
+ "gno.land/r/gov/dao/bridge"
+)
+
+const errNoChangesProposed = "no set changes proposed"
+
+// NewPropExecutor creates a new executor that wraps a changes closure
+// proposal. This wrapper is required to ensure the GovDAO Realm actually
+// executed the callback.
+//
+// Concept adapted from:
+// https://github.com/gnolang/gno/pull/1945
+func NewPropExecutor(changesFn func() []validators.Validator) dao.Executor {
+ if changesFn == nil {
+ panic(errNoChangesProposed)
+ }
+
+ callback := func() error {
+ for _, change := range changesFn() {
+ if change.VotingPower == 0 {
+ // This change request is to remove the validator
+ removeValidator(change.Address)
+
+ continue
+ }
+
+ // This change request is to add the validator
+ addValidator(change)
+ }
+
+ return nil
+ }
+
+ return bridge.GovDAO().NewGovDAOExecutor(callback)
+}
+
+// IsValidator returns a flag indicating if the given bech32 address
+// is part of the validator set
+func IsValidator(addr std.Address) bool {
+ return vp.IsValidator(addr)
+}
+
+// GetValidators returns the typed validator set
+func GetValidators() []validators.Validator {
+ return vp.GetValidators()
+}
diff --git a/examples/gno.land/r/sys/validators/validators.gno b/examples/gno.land/r/sys/validators/v2/validators.gno
similarity index 100%
rename from examples/gno.land/r/sys/validators/validators.gno
rename to examples/gno.land/r/sys/validators/v2/validators.gno
diff --git a/examples/gno.land/r/sys/validators/validators_test.gno b/examples/gno.land/r/sys/validators/v2/validators_test.gno
similarity index 100%
rename from examples/gno.land/r/sys/validators/validators_test.gno
rename to examples/gno.land/r/sys/validators/v2/validators_test.gno
diff --git a/examples/gno.land/r/ursulovic/home/gno.mod b/examples/gno.land/r/ursulovic/home/gno.mod
new file mode 100644
index 00000000000..78163ab2bb5
--- /dev/null
+++ b/examples/gno.land/r/ursulovic/home/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/ursulovic/home
diff --git a/examples/gno.land/r/ursulovic/home/home.gno b/examples/gno.land/r/ursulovic/home/home.gno
new file mode 100644
index 00000000000..c03d8a66868
--- /dev/null
+++ b/examples/gno.land/r/ursulovic/home/home.gno
@@ -0,0 +1,159 @@
+package home
+
+import (
+ "std"
+ "strconv"
+ "strings"
+
+ "gno.land/p/demo/ownable"
+ "gno.land/p/moul/md"
+ "gno.land/r/leon/hof"
+
+ "gno.land/r/ursulovic/registry"
+)
+
+var (
+ aboutMe string
+ selectedImage string
+ Ownable *ownable.Ownable
+
+ githubUrl string
+ linkedinUrl string
+ connectUrl string
+ imageUpdatePrice int64
+
+ isValidUrl func(string) bool
+)
+
+func init() {
+ Ownable = ownable.NewWithAddress(registry.MainAddress())
+
+ aboutMe = "Hi, I'm Ivan Ursulovic, a computer engineering graduate, blockchain enthusiast, and backend developer specializing in ASP.NET. I love learning new things and taking on challenges."
+ selectedImage = "https://i.ibb.co/W28NPkw/beograd.webp"
+
+ githubUrl = "https://github.com/ursulovic"
+ linkedinUrl = "https://www.linkedin.com/in/ivan-ursulovic-953310190/"
+ imageUpdatePrice = 5000000
+ isValidUrl = defaultURLValidation
+ hof.Register()
+}
+
+func Render(s string) string {
+ var sb strings.Builder
+ sb.WriteString(renderAboutMe())
+ sb.WriteString(renderSelectedImage())
+ sb.WriteString(renderContactsUrl())
+ return sb.String()
+}
+
+func defaultURLValidation(url string) bool {
+ const urlPrefix string = "https://i.ibb.co/"
+
+ if !strings.HasPrefix(url, urlPrefix) {
+ return false
+ }
+
+ if !(strings.HasSuffix(url, ".jpg") ||
+ strings.HasSuffix(url, ".png") ||
+ strings.HasSuffix(url, ".gif") ||
+ strings.HasSuffix(url, ".webp")) {
+ return false
+ }
+
+ urlPath := strings.TrimPrefix(url, "https://i.ibb.co/")
+ parts := strings.Split(urlPath, "/")
+
+ if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
+ return false
+ }
+
+ return true
+}
+
+func UpdateSelectedImage(url string) {
+ if !isValidUrl(url) {
+ panic("Url is not valid!")
+ }
+
+ sentCoins := std.GetOrigSend()
+
+ if len(sentCoins) != 1 && sentCoins.AmountOf("ugnot") == imageUpdatePrice {
+ panic("Please send exactly " + strconv.Itoa(int(imageUpdatePrice)) + " ugnot")
+ }
+
+ selectedImage = url
+}
+
+func renderSelectedImage() string {
+ var sb strings.Builder
+
+ sb.WriteString(md.HorizontalRule())
+ sb.WriteString("\n")
+
+ sb.WriteString(md.H2("📸 Featured Image"))
+ sb.WriteString("\n")
+
+ sb.WriteString(md.Image("", selectedImage))
+ sb.WriteString("\n")
+
+ sb.WriteString(md.H4("✨ " + md.Link("Change this image for "+strconv.Itoa(int(imageUpdatePrice/1000000))+" GNOT. To update, set a direct image URL from ImgBB.", "https://gno.studio/connect/view/gno.land/r/ursulovic/home?network=portal-loop") + " ✨"))
+
+ return sb.String()
+}
+
+func renderAboutMe() string {
+ var sb strings.Builder
+
+ sb.WriteString(md.H1("👋 Welcome to Ivan's Homepage!"))
+ sb.WriteString("\n")
+
+ sb.WriteString(md.H2("👨💻 About Me"))
+ sb.WriteString("\n")
+
+ sb.WriteString(md.Blockquote(aboutMe))
+
+ return sb.String()
+}
+
+func renderContactsUrl() string {
+ var sb strings.Builder
+
+ sb.WriteString(md.HorizontalRule())
+ sb.WriteString("\n")
+
+ sb.WriteString(md.H2("🔗 Let's Connect"))
+ sb.WriteString("\n")
+
+ items := []string{
+ "🐙 " + md.Link("GitHub", githubUrl),
+ "💼 " + md.Link("LinkedIn", linkedinUrl),
+ }
+ sb.WriteString(md.BulletList(items))
+
+ return sb.String()
+}
+
+func UpdateGithubUrl(url string) {
+ Ownable.AssertCallerIsOwner()
+ githubUrl = url
+}
+
+func UpdateLinkedinUrl(url string) {
+ Ownable.AssertCallerIsOwner()
+ linkedinUrl = url
+}
+
+func UpdateAboutMe(text string) {
+ Ownable.AssertCallerIsOwner()
+ aboutMe = text
+}
+
+func UpdateImagePrice(newPrice int64) {
+ Ownable.AssertCallerIsOwner()
+ imageUpdatePrice = newPrice
+}
+
+func UpdateIsValidUrlFunction(f func(string) bool) {
+ Ownable.AssertCallerIsOwner()
+ isValidUrl = f
+}
diff --git a/examples/gno.land/r/ursulovic/home/home_test.gno b/examples/gno.land/r/ursulovic/home/home_test.gno
new file mode 100644
index 00000000000..ff3f763d62a
--- /dev/null
+++ b/examples/gno.land/r/ursulovic/home/home_test.gno
@@ -0,0 +1,97 @@
+package home
+
+import (
+ "std"
+ "testing"
+
+ "gno.land/p/demo/testutils"
+)
+
+func TestUpdateGithubUrl(t *testing.T) {
+ caller := std.Address("g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x")
+ std.TestSetOrigCaller(caller)
+
+ newUrl := "https://github.com/example"
+
+ UpdateGithubUrl(newUrl)
+
+ if githubUrl != newUrl {
+ t.Fatalf("GitHub url not updated properly!")
+ }
+}
+
+func TestUpdateLinkedinUrl(t *testing.T) {
+ caller := std.Address("g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x")
+ std.TestSetOrigCaller(caller)
+
+ newUrl := "https://www.linkedin.com/in/example"
+
+ UpdateGithubUrl(newUrl)
+
+ if githubUrl != newUrl {
+ t.Fatalf("LinkedIn url not updated properly!")
+ }
+}
+
+func TestUpdateAboutMe(t *testing.T) {
+ caller := std.Address("g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x")
+ std.TestSetOrigCaller(caller)
+
+ newAboutMe := "This is new description!"
+
+ UpdateAboutMe(newAboutMe)
+
+ if aboutMe != newAboutMe {
+ t.Fatalf("About mew not updated properly!")
+ }
+}
+
+func TestUpdateSelectedImage(t *testing.T) {
+ var user = testutils.TestAddress("user")
+ std.TestSetOrigCaller(user)
+
+ validImageUrl := "https://i.ibb.co/hLtmnX0/beautiful-rain-forest-ang-ka-nature-trail-doi-inthanon-national-park-thailand-36703721.webp"
+
+ coinsSent := std.NewCoins(std.NewCoin("ugnot", 5000000)) // Update to match the price expected by your function
+ std.TestSetOrigSend(coinsSent, std.NewCoins())
+
+ UpdateSelectedImage(validImageUrl)
+
+ if selectedImage != validImageUrl {
+ t.Fatalf("Valid image URL rejected!")
+ }
+
+ invalidImageUrl := "https://ibb.co/Kb3rQNn"
+
+ defer func() {
+ if r := recover(); r == nil {
+ t.Fatalf("Expected panic for invalid image URL, but got no panic")
+ }
+ }()
+
+ UpdateSelectedImage(invalidImageUrl)
+
+ invalidCoins := std.NewCoins(std.NewCoin("ugnot", 1000000))
+ std.TestSetOrigSend(invalidCoins, std.NewCoins())
+
+ defer func() {
+ if r := recover(); r == nil {
+ t.Fatalf("Expected panic for incorrect coin denomination or amount, but got no panic")
+ }
+ }()
+
+ UpdateSelectedImage(validImageUrl)
+}
+
+func TestUpdateImagePrice(t *testing.T) {
+ caller := std.Address("g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x")
+ std.TestSetOrigCaller(caller)
+
+ var newImageUpdatePrice int64 = 3000000
+
+ UpdateImagePrice(newImageUpdatePrice)
+
+ if imageUpdatePrice != newImageUpdatePrice {
+ t.Fatalf("Image update price not updated properly!")
+ }
+}
diff --git a/examples/gno.land/r/ursulovic/registry/gno.mod b/examples/gno.land/r/ursulovic/registry/gno.mod
new file mode 100644
index 00000000000..ee1f5d38780
--- /dev/null
+++ b/examples/gno.land/r/ursulovic/registry/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/ursulovic/registry
diff --git a/examples/gno.land/r/ursulovic/registry/registry.gno b/examples/gno.land/r/ursulovic/registry/registry.gno
new file mode 100644
index 00000000000..0bbd6c80df5
--- /dev/null
+++ b/examples/gno.land/r/ursulovic/registry/registry.gno
@@ -0,0 +1,59 @@
+package registry
+
+import (
+ "errors"
+ "std"
+)
+
+var (
+ mainAddress std.Address
+ backupAddress std.Address
+
+ ErrInvalidAddr = errors.New("Ivan's registry: Invalid address")
+ ErrUnauthorized = errors.New("Ivan's registry: Unauthorized")
+)
+
+func init() {
+ mainAddress = "g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x"
+ backupAddress = "g1mw2xft3eava9kfhqw3fjj3kkf3pkammty0mtv7"
+}
+
+func MainAddress() std.Address {
+ return mainAddress
+}
+
+func BackupAddress() std.Address {
+ return backupAddress
+}
+
+func SetMainAddress(addr std.Address) error {
+ assertAuthorized()
+
+ if !addr.IsValid() {
+ return ErrInvalidAddr
+ }
+
+ mainAddress = addr
+ return nil
+}
+
+func SetBackupAddress(addr std.Address) error {
+ assertAuthorized()
+
+ if !addr.IsValid() {
+ return ErrInvalidAddr
+ }
+
+ backupAddress = addr
+ return nil
+}
+
+// It will stay here for now, might be useful later
+func assertAuthorized() {
+ caller := std.PrevRealm().Addr()
+ isAuthorized := caller == mainAddress || caller == backupAddress
+
+ if !isAuthorized {
+ panic(ErrUnauthorized)
+ }
+}
diff --git a/examples/gno.land/r/x/benchmark/storage/boards.gno b/examples/gno.land/r/x/benchmark/storage/boards.gno
new file mode 100644
index 00000000000..adb3d2d709c
--- /dev/null
+++ b/examples/gno.land/r/x/benchmark/storage/boards.gno
@@ -0,0 +1,97 @@
+package storage
+
+import (
+ "strconv"
+
+ "gno.land/p/demo/avl"
+)
+
+var boards avl.Tree
+
+type Board interface {
+ AddPost(title, content string)
+ GetPost(id int) (Post, bool)
+ Size() int
+}
+
+// posts are persisted in an avl tree
+type TreeBoard struct {
+ id int
+ posts *avl.Tree
+}
+
+func (b *TreeBoard) AddPost(title, content string) {
+ n := b.posts.Size()
+ p := Post{n, title, content}
+ b.posts.Set(strconv.Itoa(n), p)
+}
+
+func (b *TreeBoard) GetPost(id int) (Post, bool) {
+ p, ok := b.posts.Get(strconv.Itoa(id))
+ if ok {
+ return p.(Post), ok
+ } else {
+ return Post{}, ok
+ }
+}
+
+func (b *TreeBoard) Size() int {
+ return b.posts.Size()
+}
+
+// posts are persisted in a map
+type MapBoard struct {
+ id int
+ posts map[int]Post
+}
+
+func (b *MapBoard) AddPost(title, content string) {
+ n := len(b.posts)
+ p := Post{n, title, content}
+ b.posts[n] = p
+}
+
+func (b *MapBoard) GetPost(id int) (Post, bool) {
+ p, ok := b.posts[id]
+ if ok {
+ return p, ok
+ } else {
+ return Post{}, ok
+ }
+}
+
+func (b *MapBoard) Size() int {
+ return len(b.posts)
+}
+
+// posts are persisted in a slice
+type SliceBoard struct {
+ id int
+ posts []Post
+}
+
+func (b *SliceBoard) AddPost(title, content string) {
+ n := len(b.posts)
+ p := Post{n, title, content}
+ b.posts = append(b.posts, p)
+}
+
+func (b *SliceBoard) GetPost(id int) (Post, bool) {
+ if id < len(b.posts) {
+ p := b.posts[id]
+
+ return p, true
+ } else {
+ return Post{}, false
+ }
+}
+
+func (b *SliceBoard) Size() int {
+ return len(b.posts)
+}
+
+type Post struct {
+ id int
+ title string
+ content string
+}
diff --git a/examples/gno.land/r/x/benchmark/storage/forum.gno b/examples/gno.land/r/x/benchmark/storage/forum.gno
new file mode 100644
index 00000000000..8f1b3734de6
--- /dev/null
+++ b/examples/gno.land/r/x/benchmark/storage/forum.gno
@@ -0,0 +1,64 @@
+package storage
+
+import (
+ "strconv"
+
+ "gno.land/p/demo/avl"
+)
+
+func init() {
+ // we write to three common data structure for persistence
+ // avl.Tree, map and slice.
+ posts0 := avl.NewTree()
+ b0 := &TreeBoard{0, posts0}
+ boards.Set(strconv.Itoa(0), b0)
+
+ posts1 := make(map[int]Post)
+ b1 := &MapBoard{1, posts1}
+ boards.Set(strconv.Itoa(1), b1)
+
+ posts2 := []Post{}
+ b2 := &SliceBoard{2, posts2}
+ boards.Set(strconv.Itoa(2), b2)
+}
+
+// post to all boards.
+func AddPost(title, content string) {
+ for i := 0; i < boards.Size(); i++ {
+ boardId := strconv.Itoa(i)
+ b, ok := boards.Get(boardId)
+ if ok {
+ b.(Board).AddPost(title, content)
+ }
+ }
+}
+
+func GetPost(boardId, postId int) string {
+ b, ok := boards.Get(strconv.Itoa(boardId))
+ var res string
+
+ if ok {
+ p, ok := b.(Board).GetPost(postId)
+ if ok {
+ res = p.title + "," + p.content
+ }
+ }
+ return res
+}
+
+func GetPostSize(boardId int) int {
+ b, ok := boards.Get(strconv.Itoa(boardId))
+ var res int
+
+ if ok {
+ res = b.(Board).Size()
+ } else {
+ res = -1
+ }
+
+ return res
+}
+
+func GetBoardSize() int {
+ return boards.Size()
+}
diff --git a/examples/gno.land/r/x/benchmark/storage/gno.mod b/examples/gno.land/r/x/benchmark/storage/gno.mod
new file mode 100644
index 00000000000..04bea3012f3
--- /dev/null
+++ b/examples/gno.land/r/x/benchmark/storage/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/x/benchmark/storage
diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/example.gno b/examples/gno.land/r/x/jeronimo_render_proxy/example.gno
new file mode 100644
index 00000000000..7b3da098232
--- /dev/null
+++ b/examples/gno.land/r/x/jeronimo_render_proxy/example.gno
@@ -0,0 +1,20 @@
+package example
+
+func Render(string) string {
+ return `# Render Proxy
+
+This example shows how proxying render calls can be used to allow updating realms to new
+versions while keeping the same realm path. The idea is to have a simple "parent" realm
+that only keeps track of the latest realm version and forwards all render calls to it.
+
+By only focusing on the 'Render()' function the proxy realm keeps its public functions
+stable allowing each version to update their public functions and exposed types without
+needing to also update the proxy realm.
+
+Any interaction or transaction must be sent to the latest, or target, realm version while
+render calls are sent to the proxy realm.
+
+Each realm version registers itself on deployment as the latest available version, and
+its allowed to do so because each versioned realm path shares the proxy realm path.
+`
+}
diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/gno.mod b/examples/gno.land/r/x/jeronimo_render_proxy/gno.mod
new file mode 100644
index 00000000000..9236b28f5ad
--- /dev/null
+++ b/examples/gno.land/r/x/jeronimo_render_proxy/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/x/jeronimo_render_proxy
diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/home.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/home.gno
new file mode 100644
index 00000000000..c73e99cc583
--- /dev/null
+++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/home.gno
@@ -0,0 +1,52 @@
+package home
+
+import (
+ "std"
+ "strings"
+)
+
+// RenderFn defines the type for the render function of realms.
+type RenderFn func(string) string
+
+var current = struct {
+ realmPath string
+ renderFn RenderFn
+}{}
+
+// CurrentRealmPath returns the path of the realm that is currently registered.
+func CurrentRealmPath() string {
+ return current.realmPath
+}
+
+// Register registers a render function of a realm.
+func Register(fn RenderFn) {
+ if fn == nil {
+ panic("render function must not be nil")
+ }
+
+ proxyPath := std.CurrentRealm().PkgPath()
+ callerPath := std.PrevRealm().PkgPath()
+ if !strings.HasPrefix(callerPath, proxyPath+"/") {
+ panic("caller realm path must start with " + proxyPath)
+ }
+
+ current.renderFn = fn
+ current.realmPath = callerPath
+}
+
+// URL returns a URL that links to the proxy realm.
+func URL(renderPath string) string {
+ url := "http://" + std.CurrentRealm().PkgPath()
+ if renderPath != "" {
+ url += ":" + renderPath
+ }
+ return url
+}
+
+// Render renders the rendered Markdown of the realm that is currently registered.
+func Render(path string) string {
+ if current.renderFn == nil {
+ panic("no realm has been registered")
+ }
+ return current.renderFn(path)
+}
diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/v1/v1.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/v1/v1.gno
new file mode 100644
index 00000000000..8698998577c
--- /dev/null
+++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/v1/v1.gno
@@ -0,0 +1,16 @@
+package v1
+
+import "gno.land/r/x/jeronimo_render_proxy/home"
+
+func init() {
+ // Register the private render function with the render proxy
+ home.Register(render)
+}
+
+func render(string) string {
+ return "Rendered by v1"
+}
+
+func Render(string) string {
+ return "[Home](" + home.URL("") + ")"
+}
diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/v1/z_filetest.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/v1/z_filetest.gno
new file mode 100644
index 00000000000..cebe2aeb5ba
--- /dev/null
+++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/v1/z_filetest.gno
@@ -0,0 +1,10 @@
+package main
+
+import "gno.land/r/x/jeronimo_render_proxy/home/v1"
+
+func main() {
+ println(v1.Render(""))
+}
+
+// Output:
+// [Home](http://gno.land/r/x/jeronimo_render_proxy/home)
diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/v2/v2.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/v2/v2.gno
new file mode 100644
index 00000000000..031f8568441
--- /dev/null
+++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/v2/v2.gno
@@ -0,0 +1,16 @@
+package v2
+
+import "gno.land/r/x/jeronimo_render_proxy/home"
+
+func init() {
+ // Register the private render function with the render proxy
+ home.Register(render)
+}
+
+func render(string) string {
+ return "Rendered by v2"
+}
+
+func Render(string) string {
+ return "[Home](" + home.URL("") + ")"
+}
diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/v2/z_filetest.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/v2/z_filetest.gno
new file mode 100644
index 00000000000..feff15533ee
--- /dev/null
+++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/v2/z_filetest.gno
@@ -0,0 +1,10 @@
+package main
+
+import "gno.land/r/x/jeronimo_render_proxy/home/v2"
+
+func main() {
+ println(v2.Render(""))
+}
+
+// Output:
+// [Home](http://gno.land/r/x/jeronimo_render_proxy/home)
diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/z_a_filetest.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/z_a_filetest.gno
new file mode 100644
index 00000000000..c7d4d7febd2
--- /dev/null
+++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/z_a_filetest.gno
@@ -0,0 +1,10 @@
+package main
+
+import "gno.land/r/x/jeronimo_render_proxy/home"
+
+func main() {
+ home.Render("")
+}
+
+// Error:
+// no realm has been registered
diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/z_b_filetest.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/z_b_filetest.gno
new file mode 100644
index 00000000000..6ebdace67b4
--- /dev/null
+++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/z_b_filetest.gno
@@ -0,0 +1,15 @@
+package main
+
+import (
+ "gno.land/r/x/jeronimo_render_proxy/home"
+ _ "gno.land/r/x/jeronimo_render_proxy/home/v1"
+)
+
+func main() {
+ println(home.CurrentRealmPath())
+ println(home.Render(""))
+}
+
+// Output:
+// gno.land/r/x/jeronimo_render_proxy/home/v1
+// Rendered by v1
diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/z_c_filetest.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/z_c_filetest.gno
new file mode 100644
index 00000000000..f85b13bc5dd
--- /dev/null
+++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/z_c_filetest.gno
@@ -0,0 +1,16 @@
+package main
+
+import (
+ "gno.land/r/x/jeronimo_render_proxy/home"
+ _ "gno.land/r/x/jeronimo_render_proxy/home/v1"
+ _ "gno.land/r/x/jeronimo_render_proxy/home/v2"
+)
+
+func main() {
+ println(home.CurrentRealmPath())
+ println(home.Render(""))
+}
+
+// Output:
+// gno.land/r/x/jeronimo_render_proxy/home/v2
+// Rendered by v2
diff --git a/examples/gno.land/r/x/manfred_outfmt/gno.mod b/examples/gno.land/r/x/manfred_outfmt/gno.mod
index 7044f0f72b3..e8165d847c9 100644
--- a/examples/gno.land/r/x/manfred_outfmt/gno.mod
+++ b/examples/gno.land/r/x/manfred_outfmt/gno.mod
@@ -1,5 +1,3 @@
// Draft
module gno.land/r/x/manfred_outfmt
-
-require gno.land/p/demo/ufmt v0.0.0-latest
diff --git a/gno.land/Makefile b/gno.land/Makefile
index 7b2afd5779f..075560f44a9 100644
--- a/gno.land/Makefile
+++ b/gno.land/Makefile
@@ -47,6 +47,12 @@ install.gnoland:; go install ./cmd/gnoland
install.gnoweb:; go install ./cmd/gnoweb
install.gnokey:; go install ./cmd/gnokey
+.PHONY: dev.gnoweb generate.gnoweb
+dev.gnoweb:
+ make -C ./pkg/gnoweb dev
+generate.gnoweb:
+ make -C ./pkg/gnoweb generate
+
.PHONY: fclean
fclean: clean
rm -rf gnoland-data genesis.json
diff --git a/gno.land/README.md b/gno.land/README.md
index 7da2a8574de..8f7f9c32945 100644
--- a/gno.land/README.md
+++ b/gno.land/README.md
@@ -1,6 +1,6 @@
-# Gno.land
+# gno.land
-Gno.land is a layer-1 blockchain that integrates various cutting-edge technologies, including [Tendermint2](../tm2), [GnoVM](../gnovm), Proof-of-Contributions consensus mechanism, on-chain governance through a new DAO framework with support for sub-DAOs, and a unique licensing model that allows open-source code to be monetized by default.
+gno.land is a layer-1 blockchain that integrates various cutting-edge technologies, including [Tendermint2](../tm2), [GnoVM](../gnovm), Proof-of-Contributions consensus mechanism, on-chain governance through a new DAO framework with support for sub-DAOs, and a unique licensing model that allows open-source code to be monetized by default.
## Getting started
@@ -12,7 +12,7 @@ To add a web interface and faucet to your localnet, use [`gnoweb`](./cmd/gnoweb)
## Interchain
-Gno.land aims to offer security, high-quality contract libraries, and scalability to other Gnolang chains, while also prioritizing interoperability with existing and emerging chains.
+gno.land aims to offer security, high-quality contract libraries, and scalability to other Gnolang chains, while also prioritizing interoperability with existing and emerging chains.
Post mainnet launch, gno.land aims to integrate IBCv1 to connect with existing Cosmos chains and implement ICS1 for security through the existing chains.
Afterwards, the platform plans to improve IBC by adding new capabilities for interchain smart-contracts.
diff --git a/gno.land/cmd/gnoland/config_get_test.go b/gno.land/cmd/gnoland/config_get_test.go
index f2ddc5ca6d0..84cf0ba3d37 100644
--- a/gno.land/cmd/gnoland/config_get_test.go
+++ b/gno.land/cmd/gnoland/config_get_test.go
@@ -289,14 +289,6 @@ func TestConfig_Get_Base(t *testing.T) {
},
true,
},
- {
- "filter peers flag fetched",
- "filter_peers",
- func(loadedCfg *config.Config, value []byte) {
- assert.Equal(t, loadedCfg.FilterPeers, unmarshalJSONCommon[bool](t, value))
- },
- false,
- },
}
verifyGetTestTableCommon(t, testTable)
@@ -616,19 +608,11 @@ func TestConfig_Get_P2P(t *testing.T) {
},
true,
},
- {
- "upnp toggle",
- "p2p.upnp",
- func(loadedCfg *config.Config, value []byte) {
- assert.Equal(t, loadedCfg.P2P.UPNP, unmarshalJSONCommon[bool](t, value))
- },
- false,
- },
{
"max inbound peers",
"p2p.max_num_inbound_peers",
func(loadedCfg *config.Config, value []byte) {
- assert.Equal(t, loadedCfg.P2P.MaxNumInboundPeers, unmarshalJSONCommon[int](t, value))
+ assert.Equal(t, loadedCfg.P2P.MaxNumInboundPeers, unmarshalJSONCommon[uint64](t, value))
},
false,
},
@@ -636,7 +620,7 @@ func TestConfig_Get_P2P(t *testing.T) {
"max outbound peers",
"p2p.max_num_outbound_peers",
func(loadedCfg *config.Config, value []byte) {
- assert.Equal(t, loadedCfg.P2P.MaxNumOutboundPeers, unmarshalJSONCommon[int](t, value))
+ assert.Equal(t, loadedCfg.P2P.MaxNumOutboundPeers, unmarshalJSONCommon[uint64](t, value))
},
false,
},
@@ -676,15 +660,7 @@ func TestConfig_Get_P2P(t *testing.T) {
"pex reactor toggle",
"p2p.pex",
func(loadedCfg *config.Config, value []byte) {
- assert.Equal(t, loadedCfg.P2P.PexReactor, unmarshalJSONCommon[bool](t, value))
- },
- false,
- },
- {
- "seed mode",
- "p2p.seed_mode",
- func(loadedCfg *config.Config, value []byte) {
- assert.Equal(t, loadedCfg.P2P.SeedMode, unmarshalJSONCommon[bool](t, value))
+ assert.Equal(t, loadedCfg.P2P.PeerExchange, unmarshalJSONCommon[bool](t, value))
},
false,
},
@@ -704,30 +680,6 @@ func TestConfig_Get_P2P(t *testing.T) {
},
true,
},
- {
- "allow duplicate IP",
- "p2p.allow_duplicate_ip",
- func(loadedCfg *config.Config, value []byte) {
- assert.Equal(t, loadedCfg.P2P.AllowDuplicateIP, unmarshalJSONCommon[bool](t, value))
- },
- false,
- },
- {
- "handshake timeout",
- "p2p.handshake_timeout",
- func(loadedCfg *config.Config, value []byte) {
- assert.Equal(t, loadedCfg.P2P.HandshakeTimeout, unmarshalJSONCommon[time.Duration](t, value))
- },
- false,
- },
- {
- "dial timeout",
- "p2p.dial_timeout",
- func(loadedCfg *config.Config, value []byte) {
- assert.Equal(t, loadedCfg.P2P.DialTimeout, unmarshalJSONCommon[time.Duration](t, value))
- },
- false,
- },
}
verifyGetTestTableCommon(t, testTable)
diff --git a/gno.land/cmd/gnoland/config_set_test.go b/gno.land/cmd/gnoland/config_set_test.go
index cb831f0e502..39880313043 100644
--- a/gno.land/cmd/gnoland/config_set_test.go
+++ b/gno.land/cmd/gnoland/config_set_test.go
@@ -244,19 +244,6 @@ func TestConfig_Set_Base(t *testing.T) {
assert.Equal(t, value, loadedCfg.ProfListenAddress)
},
},
- {
- "filter peers flag updated",
- []string{
- "filter_peers",
- "true",
- },
- func(loadedCfg *config.Config, value string) {
- boolVal, err := strconv.ParseBool(value)
- require.NoError(t, err)
-
- assert.Equal(t, boolVal, loadedCfg.FilterPeers)
- },
- },
}
verifySetTestTableCommon(t, testTable)
@@ -505,19 +492,6 @@ func TestConfig_Set_P2P(t *testing.T) {
assert.Equal(t, value, loadedCfg.P2P.PersistentPeers)
},
},
- {
- "upnp toggle updated",
- []string{
- "p2p.upnp",
- "false",
- },
- func(loadedCfg *config.Config, value string) {
- boolVal, err := strconv.ParseBool(value)
- require.NoError(t, err)
-
- assert.Equal(t, boolVal, loadedCfg.P2P.UPNP)
- },
- },
{
"max inbound peers updated",
[]string{
@@ -588,20 +562,7 @@ func TestConfig_Set_P2P(t *testing.T) {
boolVal, err := strconv.ParseBool(value)
require.NoError(t, err)
- assert.Equal(t, boolVal, loadedCfg.P2P.PexReactor)
- },
- },
- {
- "seed mode updated",
- []string{
- "p2p.seed_mode",
- "false",
- },
- func(loadedCfg *config.Config, value string) {
- boolVal, err := strconv.ParseBool(value)
- require.NoError(t, err)
-
- assert.Equal(t, boolVal, loadedCfg.P2P.SeedMode)
+ assert.Equal(t, boolVal, loadedCfg.P2P.PeerExchange)
},
},
{
@@ -614,39 +575,6 @@ func TestConfig_Set_P2P(t *testing.T) {
assert.Equal(t, value, loadedCfg.P2P.PrivatePeerIDs)
},
},
- {
- "allow duplicate IPs updated",
- []string{
- "p2p.allow_duplicate_ip",
- "false",
- },
- func(loadedCfg *config.Config, value string) {
- boolVal, err := strconv.ParseBool(value)
- require.NoError(t, err)
-
- assert.Equal(t, boolVal, loadedCfg.P2P.AllowDuplicateIP)
- },
- },
- {
- "handshake timeout updated",
- []string{
- "p2p.handshake_timeout",
- "1s",
- },
- func(loadedCfg *config.Config, value string) {
- assert.Equal(t, value, loadedCfg.P2P.HandshakeTimeout.String())
- },
- },
- {
- "dial timeout updated",
- []string{
- "p2p.dial_timeout",
- "1s",
- },
- func(loadedCfg *config.Config, value string) {
- assert.Equal(t, value, loadedCfg.P2P.DialTimeout.String())
- },
- },
}
verifySetTestTableCommon(t, testTable)
diff --git a/gno.land/cmd/gnoland/genesis.go b/gno.land/cmd/gnoland/genesis.go
deleted file mode 100644
index 37c0f8f2926..00000000000
--- a/gno.land/cmd/gnoland/genesis.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package main
-
-import (
- "flag"
-
- "github.com/gnolang/gno/tm2/pkg/commands"
-)
-
-func newGenesisCmd(io commands.IO) *commands.Command {
- cmd := commands.NewCommand(
- commands.Metadata{
- Name: "genesis",
- ShortUsage: "genesis
[flags] [...]",
- ShortHelp: "gno genesis manipulation suite",
- LongHelp: "Gno genesis.json manipulation suite, for managing genesis parameters",
- },
- commands.NewEmptyConfig(),
- commands.HelpExec,
- )
-
- cmd.AddSubCommands(
- newGenerateCmd(io),
- newValidatorCmd(io),
- newVerifyCmd(io),
- newBalancesCmd(io),
- newTxsCmd(io),
- )
-
- return cmd
-}
-
-// commonCfg is the common
-// configuration for genesis commands
-// that require a genesis.json
-type commonCfg struct {
- genesisPath string
-}
-
-func (c *commonCfg) RegisterFlags(fs *flag.FlagSet) {
- fs.StringVar(
- &c.genesisPath,
- "genesis-path",
- "./genesis.json",
- "the path to the genesis.json",
- )
-}
diff --git a/gno.land/cmd/gnoland/genesis_balances.go b/gno.land/cmd/gnoland/genesis_balances.go
deleted file mode 100644
index c8cd1c539f5..00000000000
--- a/gno.land/cmd/gnoland/genesis_balances.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package main
-
-import (
- "flag"
-
- "github.com/gnolang/gno/tm2/pkg/commands"
-)
-
-type balancesCfg struct {
- commonCfg
-}
-
-// newBalancesCmd creates the genesis balances subcommand
-func newBalancesCmd(io commands.IO) *commands.Command {
- cfg := &balancesCfg{}
-
- cmd := commands.NewCommand(
- commands.Metadata{
- Name: "balances",
- ShortUsage: "balances [flags]",
- ShortHelp: "manages genesis.json account balances",
- LongHelp: "Manipulates the initial genesis.json account balances (pre-mines)",
- },
- cfg,
- commands.HelpExec,
- )
-
- cmd.AddSubCommands(
- newBalancesAddCmd(cfg, io),
- newBalancesRemoveCmd(cfg, io),
- newBalancesExportCmd(cfg, io),
- )
-
- return cmd
-}
-
-func (c *balancesCfg) RegisterFlags(fs *flag.FlagSet) {
- c.commonCfg.RegisterFlags(fs)
-}
diff --git a/gno.land/cmd/gnoland/genesis_balances_add.go b/gno.land/cmd/gnoland/genesis_balances_add.go
deleted file mode 100644
index f9a898715c8..00000000000
--- a/gno.land/cmd/gnoland/genesis_balances_add.go
+++ /dev/null
@@ -1,298 +0,0 @@
-package main
-
-import (
- "bufio"
- "context"
- "errors"
- "flag"
- "fmt"
- "io"
- "os"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
- "github.com/gnolang/gno/tm2/pkg/amino"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/sdk/bank"
- "github.com/gnolang/gno/tm2/pkg/std"
-
- _ "github.com/gnolang/gno/gno.land/pkg/sdk/vm"
-)
-
-var (
- errNoBalanceSource = errors.New("at least one balance source must be set")
- errBalanceParsingAborted = errors.New("balance parsing aborted")
- errInvalidAddress = errors.New("invalid address encountered")
-)
-
-type balancesAddCfg struct {
- rootCfg *balancesCfg
-
- balanceSheet string
- singleEntries commands.StringArr
- parseExport string
-}
-
-// newBalancesAddCmd creates the genesis balances add subcommand
-func newBalancesAddCmd(rootCfg *balancesCfg, io commands.IO) *commands.Command {
- cfg := &balancesAddCfg{
- rootCfg: rootCfg,
- }
-
- return commands.NewCommand(
- commands.Metadata{
- Name: "add",
- ShortUsage: "balances add [flags]",
- ShortHelp: "adds balances to the genesis.json",
- },
- cfg,
- func(ctx context.Context, _ []string) error {
- return execBalancesAdd(ctx, cfg, io)
- },
- )
-}
-
-func (c *balancesAddCfg) RegisterFlags(fs *flag.FlagSet) {
- fs.StringVar(
- &c.balanceSheet,
- "balance-sheet",
- "",
- "the path to the balance file containing addresses in the format ="+ugnot.Denom,
- )
-
- fs.Var(
- &c.singleEntries,
- "single",
- "the direct balance addition in the format ="+ugnot.Denom,
- )
-
- fs.StringVar(
- &c.parseExport,
- "parse-export",
- "",
- "the path to the transaction export containing a list of transactions (JSONL)",
- )
-}
-
-func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io commands.IO) error {
- // Load the genesis
- genesis, loadErr := types.GenesisDocFromFile(cfg.rootCfg.genesisPath)
- if loadErr != nil {
- return fmt.Errorf("unable to load genesis, %w", loadErr)
- }
-
- // Validate the source is set correctly
- var (
- singleEntriesSet = len(cfg.singleEntries) != 0
- balanceSheetSet = cfg.balanceSheet != ""
- txFileSet = cfg.parseExport != ""
- )
-
- if !singleEntriesSet && !balanceSheetSet && !txFileSet {
- return errNoBalanceSource
- }
-
- finalBalances := gnoland.NewBalances()
-
- // Get the balance sheet from the source
- if singleEntriesSet {
- balances, err := gnoland.GetBalancesFromEntries(cfg.singleEntries...)
- if err != nil {
- return fmt.Errorf("unable to get balances from entries, %w", err)
- }
-
- finalBalances.LeftMerge(balances)
- }
-
- if balanceSheetSet {
- // Open the balance sheet
- file, loadErr := os.Open(cfg.balanceSheet)
- if loadErr != nil {
- return fmt.Errorf("unable to open balance sheet, %w", loadErr)
- }
-
- balances, err := gnoland.GetBalancesFromSheet(file)
- if err != nil {
- return fmt.Errorf("unable to get balances from balance sheet, %w", err)
- }
-
- finalBalances.LeftMerge(balances)
- }
-
- if txFileSet {
- // Open the transactions file
- file, loadErr := os.Open(cfg.parseExport)
- if loadErr != nil {
- return fmt.Errorf("unable to open transactions file, %w", loadErr)
- }
-
- balances, err := getBalancesFromTransactions(ctx, io, file)
- if err != nil {
- return fmt.Errorf("unable to get balances from tx file, %w", err)
- }
-
- finalBalances.LeftMerge(balances)
- }
-
- // Initialize genesis app state if it is not initialized already
- if genesis.AppState == nil {
- genesis.AppState = gnoland.GnoGenesisState{}
- }
-
- // Construct the initial genesis balance sheet
- state := genesis.AppState.(gnoland.GnoGenesisState)
- genesisBalances, err := mapGenesisBalancesFromState(state)
- if err != nil {
- return err
- }
-
- // Merge the two balance sheets, with the input
- // having precedence over the genesis balances
- finalBalances.LeftMerge(genesisBalances)
-
- // Save the balances
- state.Balances = finalBalances.List()
- genesis.AppState = state
-
- // Save the updated genesis
- if err := genesis.SaveAs(cfg.rootCfg.genesisPath); err != nil {
- return fmt.Errorf("unable to save genesis.json, %w", err)
- }
-
- io.Printfln(
- "%d pre-mines saved",
- len(finalBalances),
- )
-
- io.Println()
-
- for address, balance := range finalBalances {
- io.Printfln("%s:%d%s", address.String(), balance, ugnot.Denom)
- }
-
- return nil
-}
-
-// getBalancesFromTransactions constructs a balance map based on MsgSend messages.
-// This way of determining the final balance sheet is not valid, since it doesn't take into
-// account different message types (ex. MsgCall) that can initialize accounts with some balance values.
-// The right way to do this sort of initialization is to spin up an in-memory node
-// and execute the entire transaction history to determine touched accounts and final balances,
-// and construct a balance sheet based off of this information
-func getBalancesFromTransactions(
- ctx context.Context,
- io commands.IO,
- reader io.Reader,
-) (gnoland.Balances, error) {
- balances := gnoland.NewBalances()
-
- scanner := bufio.NewScanner(reader)
-
- for scanner.Scan() {
- select {
- case <-ctx.Done():
- return nil, errBalanceParsingAborted
- default:
- // Parse the amino JSON
- var tx std.Tx
-
- line := scanner.Bytes()
-
- if err := amino.UnmarshalJSON(line, &tx); err != nil {
- io.ErrPrintfln(
- "invalid amino JSON encountered: %q",
- string(line),
- )
-
- continue
- }
-
- feeAmount := std.NewCoins(tx.Fee.GasFee)
- if feeAmount.AmountOf(ugnot.Denom) <= 0 {
- io.ErrPrintfln(
- "invalid gas fee amount encountered: %q",
- tx.Fee.GasFee.String(),
- )
- }
-
- for _, msg := range tx.Msgs {
- if msg.Type() != "send" {
- continue
- }
-
- msgSend := msg.(bank.MsgSend)
-
- sendAmount := msgSend.Amount
- if sendAmount.AmountOf(ugnot.Denom) <= 0 {
- io.ErrPrintfln(
- "invalid send amount encountered: %s",
- msgSend.Amount.String(),
- )
- continue
- }
-
- // This way of determining final account balances is not really valid,
- // because we take into account only the ugnot transfer messages (MsgSend)
- // and not other message types (like MsgCall), that can also
- // initialize accounts with some gnoland. Because of this,
- // we can run into a situation where a message send amount or fee
- // causes an accounts balance to go < 0. In these cases,
- // we initialize the account (it is present in the balance sheet), but
- // with the balance of 0
-
- from := balances[msgSend.FromAddress].Amount
- to := balances[msgSend.ToAddress].Amount
-
- to = to.Add(sendAmount)
-
- if from.IsAllLT(sendAmount) || from.IsAllLT(feeAmount) {
- // Account cannot cover send amount / fee
- // (see message above)
- from = std.NewCoins(std.NewCoin(ugnot.Denom, 0))
- }
-
- if from.IsAllGT(sendAmount) {
- from = from.Sub(sendAmount)
- }
-
- if from.IsAllGT(feeAmount) {
- from = from.Sub(feeAmount)
- }
-
- // Set new balance
- balances[msgSend.FromAddress] = gnoland.Balance{
- Address: msgSend.FromAddress,
- Amount: from,
- }
- balances[msgSend.ToAddress] = gnoland.Balance{
- Address: msgSend.ToAddress,
- Amount: to,
- }
- }
- }
- }
-
- // Check for scanning errors
- if err := scanner.Err(); err != nil {
- return nil, fmt.Errorf(
- "error encountered while reading file, %w",
- err,
- )
- }
-
- return balances, nil
-}
-
-// mapGenesisBalancesFromState extracts the initial account balances from the
-// genesis app state
-func mapGenesisBalancesFromState(state gnoland.GnoGenesisState) (gnoland.Balances, error) {
- // Construct the initial genesis balance sheet
- genesisBalances := gnoland.NewBalances()
-
- for _, balance := range state.Balances {
- genesisBalances[balance.Address] = balance
- }
-
- return genesisBalances, nil
-}
diff --git a/gno.land/cmd/gnoland/genesis_balances_add_test.go b/gno.land/cmd/gnoland/genesis_balances_add_test.go
deleted file mode 100644
index 8f2879f9c57..00000000000
--- a/gno.land/cmd/gnoland/genesis_balances_add_test.go
+++ /dev/null
@@ -1,581 +0,0 @@
-package main
-
-import (
- "bytes"
- "context"
- "fmt"
- "strings"
- "testing"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
- "github.com/gnolang/gno/tm2/pkg/amino"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/sdk/bank"
- "github.com/gnolang/gno/tm2/pkg/std"
- "github.com/gnolang/gno/tm2/pkg/testutils"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestGenesis_Balances_Add(t *testing.T) {
- t.Parallel()
-
- t.Run("invalid genesis", func(t *testing.T) {
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "balances",
- "add",
- "--genesis-path",
- "dummy-path",
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error())
- })
-
- t.Run("no sources selected", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "balances",
- "add",
- "--genesis-path",
- tempGenesis.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errNoBalanceSource.Error())
- })
-
- t.Run("invalid genesis path", func(t *testing.T) {
- t.Parallel()
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "balances",
- "add",
- "--genesis-path",
- "dummy-path",
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error())
- })
-
- t.Run("balances from entries", func(t *testing.T) {
- t.Parallel()
-
- dummyKeys := getDummyKeys(t, 2)
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "balances",
- "add",
- "--genesis-path",
- tempGenesis.Name(),
- }
-
- amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10))
-
- for _, dummyKey := range dummyKeys {
- args = append(args, "--single")
- args = append(
- args,
- fmt.Sprintf(
- "%s=%s",
- dummyKey.Address().String(),
- ugnot.ValueString(amount.AmountOf(ugnot.Denom)),
- ),
- )
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Validate the genesis was updated
- genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name())
- require.NoError(t, loadErr)
-
- require.NotNil(t, genesis.AppState)
-
- state, ok := genesis.AppState.(gnoland.GnoGenesisState)
- require.True(t, ok)
-
- require.Equal(t, len(dummyKeys), len(state.Balances))
-
- for _, balance := range state.Balances {
- // Find the appropriate key
- // (the genesis is saved with randomized balance order)
- found := false
- for _, dummyKey := range dummyKeys {
- if dummyKey.Address().String() == balance.Address.String() {
- assert.Equal(t, amount, balance.Amount)
-
- found = true
- break
- }
- }
-
- if !found {
- t.Fatalf("unexpected entry with address %s found", balance.Address.String())
- }
- }
- })
-
- t.Run("balances from sheet", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- dummyKeys := getDummyKeys(t, 10)
- amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10))
-
- balances := make([]string, len(dummyKeys))
-
- // Add a random comment to the balances file output
- balances = append(balances, "#comment\n")
-
- for index, key := range dummyKeys {
- balances[index] = fmt.Sprintf(
- "%s=%s",
- key.Address().String(),
- ugnot.ValueString(amount.AmountOf(ugnot.Denom)),
- )
- }
-
- // Write the balance sheet to a file
- balanceSheet, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- _, err := balanceSheet.WriteString(strings.Join(balances, "\n"))
- require.NoError(t, err)
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "balances",
- "add",
- "--genesis-path",
- tempGenesis.Name(),
- "--balance-sheet",
- balanceSheet.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Validate the genesis was updated
- genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name())
- require.NoError(t, loadErr)
-
- require.NotNil(t, genesis.AppState)
-
- state, ok := genesis.AppState.(gnoland.GnoGenesisState)
- require.True(t, ok)
-
- require.Equal(t, len(dummyKeys), len(state.Balances))
-
- for _, balance := range state.Balances {
- // Find the appropriate key
- // (the genesis is saved with randomized balance order)
- found := false
- for _, dummyKey := range dummyKeys {
- if dummyKey.Address().String() == balance.Address.String() {
- assert.Equal(t, amount, balance.Amount)
-
- found = true
- break
- }
- }
-
- if !found {
- t.Fatalf("unexpected entry with address %s found", balance.Address.String())
- }
- }
- })
-
- t.Run("balances from transactions", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- var (
- dummyKeys = getDummyKeys(t, 10)
- amount = std.NewCoins(std.NewCoin(ugnot.Denom, 10))
- amountCoins = std.NewCoins(std.NewCoin(ugnot.Denom, 10))
- gasFee = std.NewCoin(ugnot.Denom, 1000000)
- txs = make([]std.Tx, 0)
- )
-
- sender := dummyKeys[0]
- for _, dummyKey := range dummyKeys[1:] {
- tx := std.Tx{
- Msgs: []std.Msg{
- bank.MsgSend{
- FromAddress: sender.Address(),
- ToAddress: dummyKey.Address(),
- Amount: amountCoins,
- },
- },
- Fee: std.Fee{
- GasWanted: 10,
- GasFee: gasFee,
- },
- Signatures: make([]std.Signature, 0),
- }
-
- txs = append(txs, tx)
- }
-
- // Marshal the transactions into amino JSON
- marshalledTxs := make([]string, 0, len(txs))
-
- for _, tx := range txs {
- marshalledTx, err := amino.MarshalJSON(tx)
- require.NoError(t, err)
-
- marshalledTxs = append(marshalledTxs, string(marshalledTx))
- }
-
- // Write the transactions to a file
- txsFile, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- _, err := txsFile.WriteString(strings.Join(marshalledTxs, "\n"))
- require.NoError(t, err)
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "balances",
- "add",
- "--genesis-path",
- tempGenesis.Name(),
- "--parse-export",
- txsFile.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Validate the genesis was updated
- genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name())
- require.NoError(t, loadErr)
-
- require.NotNil(t, genesis.AppState)
-
- state, ok := genesis.AppState.(gnoland.GnoGenesisState)
- require.True(t, ok)
-
- require.Equal(t, len(dummyKeys), len(state.Balances))
-
- for _, balance := range state.Balances {
- // Find the appropriate key
- // (the genesis is saved with randomized balance order)
- found := false
- for index, dummyKey := range dummyKeys {
- checkAmount := amount
- if index == 0 {
- // the first address should
- // have a balance of 0
- checkAmount = std.NewCoins(std.NewCoin(ugnot.Denom, 0))
- }
-
- if dummyKey.Address().String() == balance.Address.String() {
- assert.True(t, balance.Amount.IsEqual(checkAmount))
-
- found = true
- break
- }
- }
-
- if !found {
- t.Fatalf("unexpected entry with address %s found", balance.Address.String())
- }
- }
- })
-
- t.Run("balances overwrite", func(t *testing.T) {
- t.Parallel()
-
- dummyKeys := getDummyKeys(t, 10)
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- state := gnoland.GnoGenesisState{
- // Set an initial balance value
- Balances: []gnoland.Balance{
- {
- Address: dummyKeys[0].Address(),
- Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 100)),
- },
- },
- }
- genesis.AppState = state
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "balances",
- "add",
- "--genesis-path",
- tempGenesis.Name(),
- }
-
- amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10))
-
- for _, dummyKey := range dummyKeys {
- args = append(args, "--single")
- args = append(
- args,
- fmt.Sprintf(
- "%s=%s",
- dummyKey.Address().String(),
- ugnot.ValueString(amount.AmountOf(ugnot.Denom)),
- ),
- )
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Validate the genesis was updated
- genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name())
- require.NoError(t, loadErr)
-
- require.NotNil(t, genesis.AppState)
-
- state, ok := genesis.AppState.(gnoland.GnoGenesisState)
- require.True(t, ok)
-
- require.Equal(t, len(dummyKeys), len(state.Balances))
-
- for _, balance := range state.Balances {
- // Find the appropriate key
- // (the genesis is saved with randomized balance order)
- found := false
- for _, dummyKey := range dummyKeys {
- if dummyKey.Address().String() == balance.Address.String() {
- assert.Equal(t, amount, balance.Amount)
-
- found = true
- break
- }
- }
-
- if !found {
- t.Fatalf("unexpected entry with address %s found", balance.Address.String())
- }
- }
- })
-}
-
-func TestBalances_GetBalancesFromTransactions(t *testing.T) {
- t.Parallel()
-
- t.Run("valid transactions", func(t *testing.T) {
- t.Parallel()
-
- var (
- dummyKeys = getDummyKeys(t, 10)
- amount = std.NewCoins(std.NewCoin(ugnot.Denom, 10))
- amountCoins = std.NewCoins(std.NewCoin(ugnot.Denom, 10))
- gasFee = std.NewCoin(ugnot.Denom, 1000000)
- txs = make([]std.Tx, 0)
- )
-
- sender := dummyKeys[0]
- for _, dummyKey := range dummyKeys[1:] {
- tx := std.Tx{
- Msgs: []std.Msg{
- bank.MsgSend{
- FromAddress: sender.Address(),
- ToAddress: dummyKey.Address(),
- Amount: amountCoins,
- },
- },
- Fee: std.Fee{
- GasWanted: 10,
- GasFee: gasFee,
- },
- Signatures: make([]std.Signature, 0),
- }
-
- txs = append(txs, tx)
- }
-
- // Marshal the transactions into amino JSON
- marshalledTxs := make([]string, 0, len(txs))
-
- for _, tx := range txs {
- marshalledTx, err := amino.MarshalJSON(tx)
- require.NoError(t, err)
-
- marshalledTxs = append(marshalledTxs, string(marshalledTx))
- }
-
- mockErr := new(bytes.Buffer)
- io := commands.NewTestIO()
- io.SetErr(commands.WriteNopCloser(mockErr))
-
- reader := strings.NewReader(strings.Join(marshalledTxs, "\n"))
- balanceMap, err := getBalancesFromTransactions(context.Background(), io, reader)
- require.NoError(t, err)
-
- // Validate the balance map
- assert.Len(t, balanceMap, len(dummyKeys))
- for _, key := range dummyKeys[1:] {
- assert.Equal(t, amount, balanceMap[key.Address()].Amount)
- }
-
- assert.Equal(t, std.Coins{}, balanceMap[sender.Address()].Amount)
- })
-
- t.Run("malformed transaction, invalid fee amount", func(t *testing.T) {
- t.Parallel()
-
- var (
- dummyKeys = getDummyKeys(t, 10)
- amountCoins = std.NewCoins(std.NewCoin(ugnot.Denom, 10))
- gasFee = std.NewCoin("gnos", 1) // invalid fee
- txs = make([]std.Tx, 0)
- )
-
- sender := dummyKeys[0]
- for _, dummyKey := range dummyKeys[1:] {
- tx := std.Tx{
- Msgs: []std.Msg{
- bank.MsgSend{
- FromAddress: sender.Address(),
- ToAddress: dummyKey.Address(),
- Amount: amountCoins,
- },
- },
- Fee: std.Fee{
- GasWanted: 10,
- GasFee: gasFee,
- },
- Signatures: make([]std.Signature, 0),
- }
-
- txs = append(txs, tx)
- }
-
- // Marshal the transactions into amino JSON
- marshalledTxs := make([]string, 0, len(txs))
-
- for _, tx := range txs {
- marshalledTx, err := amino.MarshalJSON(tx)
- require.NoError(t, err)
-
- marshalledTxs = append(marshalledTxs, string(marshalledTx))
- }
-
- mockErr := new(bytes.Buffer)
- io := commands.NewTestIO()
- io.SetErr(commands.WriteNopCloser(mockErr))
-
- reader := strings.NewReader(strings.Join(marshalledTxs, "\n"))
- balanceMap, err := getBalancesFromTransactions(context.Background(), io, reader)
- require.NoError(t, err)
-
- assert.NotNil(t, balanceMap)
- assert.Contains(t, mockErr.String(), "invalid gas fee amount")
- })
-
- t.Run("malformed transaction, invalid send amount", func(t *testing.T) {
- t.Parallel()
-
- var (
- dummyKeys = getDummyKeys(t, 10)
- amountCoins = std.NewCoins(std.NewCoin("gnogno", 10)) // invalid send amount
- gasFee = std.NewCoin(ugnot.Denom, 1)
- txs = make([]std.Tx, 0)
- )
-
- sender := dummyKeys[0]
- for _, dummyKey := range dummyKeys[1:] {
- tx := std.Tx{
- Msgs: []std.Msg{
- bank.MsgSend{
- FromAddress: sender.Address(),
- ToAddress: dummyKey.Address(),
- Amount: amountCoins,
- },
- },
- Fee: std.Fee{
- GasWanted: 10,
- GasFee: gasFee,
- },
- Signatures: make([]std.Signature, 0),
- }
-
- txs = append(txs, tx)
- }
-
- // Marshal the transactions into amino JSON
- marshalledTxs := make([]string, 0, len(txs))
-
- for _, tx := range txs {
- marshalledTx, err := amino.MarshalJSON(tx)
- require.NoError(t, err)
-
- marshalledTxs = append(marshalledTxs, string(marshalledTx))
- }
-
- mockErr := new(bytes.Buffer)
- io := commands.NewTestIO()
- io.SetErr(commands.WriteNopCloser(mockErr))
-
- reader := strings.NewReader(strings.Join(marshalledTxs, "\n"))
- balanceMap, err := getBalancesFromTransactions(context.Background(), io, reader)
- require.NoError(t, err)
-
- assert.NotNil(t, balanceMap)
- assert.Contains(t, mockErr.String(), "invalid send amount")
- })
-}
diff --git a/gno.land/cmd/gnoland/genesis_balances_export.go b/gno.land/cmd/gnoland/genesis_balances_export.go
deleted file mode 100644
index ec05d115b97..00000000000
--- a/gno.land/cmd/gnoland/genesis_balances_export.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package main
-
-import (
- "context"
- "fmt"
- "os"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
-)
-
-// newBalancesExportCmd creates the genesis balances export subcommand
-func newBalancesExportCmd(balancesCfg *balancesCfg, io commands.IO) *commands.Command {
- return commands.NewCommand(
- commands.Metadata{
- Name: "export",
- ShortUsage: "balances export [flags] ",
- ShortHelp: "exports the balances from the genesis.json",
- LongHelp: "Exports the balances from the genesis.json to an output file",
- },
- commands.NewEmptyConfig(),
- func(_ context.Context, args []string) error {
- return execBalancesExport(balancesCfg, io, args)
- },
- )
-}
-
-func execBalancesExport(cfg *balancesCfg, io commands.IO, args []string) error {
- // Load the genesis
- genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath)
- if loadErr != nil {
- return fmt.Errorf("unable to load genesis, %w", loadErr)
- }
-
- // Load the genesis state
- if genesis.AppState == nil {
- return errAppStateNotSet
- }
-
- state := genesis.AppState.(gnoland.GnoGenesisState)
- if len(state.Balances) == 0 {
- io.Println("No genesis balances to export")
-
- return nil
- }
-
- // Make sure the output file path is specified
- if len(args) == 0 {
- return errNoOutputFile
- }
-
- // Open output file
- outputFile, err := os.OpenFile(
- args[0],
- os.O_RDWR|os.O_CREATE|os.O_APPEND,
- 0o755,
- )
- if err != nil {
- return fmt.Errorf("unable to create output file, %w", err)
- }
-
- // Save the balances
- for _, balance := range state.Balances {
- if _, err = outputFile.WriteString(
- fmt.Sprintf("%s\n", balance),
- ); err != nil {
- return fmt.Errorf("unable to write to output, %w", err)
- }
- }
-
- io.Printfln(
- "Exported %d balances",
- len(state.Balances),
- )
-
- return nil
-}
diff --git a/gno.land/cmd/gnoland/genesis_balances_export_test.go b/gno.land/cmd/gnoland/genesis_balances_export_test.go
deleted file mode 100644
index bd1f6152246..00000000000
--- a/gno.land/cmd/gnoland/genesis_balances_export_test.go
+++ /dev/null
@@ -1,163 +0,0 @@
-package main
-
-import (
- "bufio"
- "context"
- "testing"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/std"
- "github.com/gnolang/gno/tm2/pkg/testutils"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-// getDummyBalances generates dummy balance lines
-func getDummyBalances(t *testing.T, count int) []gnoland.Balance {
- t.Helper()
-
- dummyKeys := getDummyKeys(t, count)
- amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10))
-
- balances := make([]gnoland.Balance, len(dummyKeys))
-
- for index, key := range dummyKeys {
- balances[index] = gnoland.Balance{
- Address: key.Address(),
- Amount: amount,
- }
- }
-
- return balances
-}
-
-func TestGenesis_Balances_Export(t *testing.T) {
- t.Parallel()
-
- t.Run("invalid genesis file", func(t *testing.T) {
- t.Parallel()
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "balances",
- "export",
- "--genesis-path",
- "dummy-path",
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error())
- })
-
- t.Run("invalid genesis app state", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- genesis.AppState = nil // no app state
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "balances",
- "export",
- "--genesis-path",
- tempGenesis.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errAppStateNotSet.Error())
- })
-
- t.Run("no output file specified", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- genesis.AppState = gnoland.GnoGenesisState{
- Balances: getDummyBalances(t, 1),
- }
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "balances",
- "export",
- "--genesis-path",
- tempGenesis.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errNoOutputFile.Error())
- })
-
- t.Run("valid balances export", func(t *testing.T) {
- t.Parallel()
-
- // Generate dummy balances
- balances := getDummyBalances(t, 10)
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- genesis.AppState = gnoland.GnoGenesisState{
- Balances: balances,
- }
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Prepare the output file
- outputFile, outputCleanup := testutils.NewTestFile(t)
- t.Cleanup(outputCleanup)
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "balances",
- "export",
- "--genesis-path",
- tempGenesis.Name(),
- outputFile.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Validate the transactions were written down
- scanner := bufio.NewScanner(outputFile)
-
- outputBalances := make([]gnoland.Balance, 0)
- for scanner.Scan() {
- var balance gnoland.Balance
- err := balance.Parse(scanner.Text())
- require.NoError(t, err)
-
- outputBalances = append(outputBalances, balance)
- }
-
- require.NoError(t, scanner.Err())
-
- assert.Len(t, outputBalances, len(balances))
-
- for index, balance := range outputBalances {
- assert.Equal(t, balances[index], balance)
- }
- })
-}
diff --git a/gno.land/cmd/gnoland/genesis_balances_remove.go b/gno.land/cmd/gnoland/genesis_balances_remove.go
deleted file mode 100644
index 58a02319c8d..00000000000
--- a/gno.land/cmd/gnoland/genesis_balances_remove.go
+++ /dev/null
@@ -1,103 +0,0 @@
-package main
-
-import (
- "context"
- "errors"
- "flag"
- "fmt"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/crypto"
-)
-
-var (
- errUnableToLoadGenesis = errors.New("unable to load genesis")
- errBalanceNotFound = errors.New("genesis balances entry does not exist")
-)
-
-type balancesRemoveCfg struct {
- rootCfg *balancesCfg
-
- address string
-}
-
-// newBalancesRemoveCmd creates the genesis balances remove subcommand
-func newBalancesRemoveCmd(rootCfg *balancesCfg, io commands.IO) *commands.Command {
- cfg := &balancesRemoveCfg{
- rootCfg: rootCfg,
- }
-
- return commands.NewCommand(
- commands.Metadata{
- Name: "remove",
- ShortUsage: "balances remove [flags]",
- ShortHelp: "removes the balance information of a specific account",
- },
- cfg,
- func(_ context.Context, _ []string) error {
- return execBalancesRemove(cfg, io)
- },
- )
-}
-
-func (c *balancesRemoveCfg) RegisterFlags(fs *flag.FlagSet) {
- fs.StringVar(
- &c.address,
- "address",
- "",
- "the address of the account whose balance information should be removed from genesis.json",
- )
-}
-
-func execBalancesRemove(cfg *balancesRemoveCfg, io commands.IO) error {
- // Load the genesis
- genesis, loadErr := types.GenesisDocFromFile(cfg.rootCfg.genesisPath)
- if loadErr != nil {
- return fmt.Errorf("%w, %w", errUnableToLoadGenesis, loadErr)
- }
-
- // Validate the address
- address, err := crypto.AddressFromString(cfg.address)
- if err != nil {
- return fmt.Errorf("%w, %w", errInvalidAddress, err)
- }
-
- // Check if the genesis state is set at all
- if genesis.AppState == nil {
- return errAppStateNotSet
- }
-
- // Construct the initial genesis balance sheet
- state := genesis.AppState.(gnoland.GnoGenesisState)
- genesisBalances, err := mapGenesisBalancesFromState(state)
- if err != nil {
- return err
- }
-
- // Check if the genesis balance for the account is present
- _, exists := genesisBalances[address]
- if !exists {
- return errBalanceNotFound
- }
-
- // Drop the account pre-mine
- delete(genesisBalances, address)
-
- // Save the balances
- state.Balances = genesisBalances.List()
- genesis.AppState = state
-
- // Save the updated genesis
- if err := genesis.SaveAs(cfg.rootCfg.genesisPath); err != nil {
- return fmt.Errorf("unable to save genesis.json, %w", err)
- }
-
- io.Printfln(
- "Pre-mine information for address %s removed",
- address.String(),
- )
-
- return nil
-}
diff --git a/gno.land/cmd/gnoland/genesis_balances_remove_test.go b/gno.land/cmd/gnoland/genesis_balances_remove_test.go
deleted file mode 100644
index ed11836ba4d..00000000000
--- a/gno.land/cmd/gnoland/genesis_balances_remove_test.go
+++ /dev/null
@@ -1,145 +0,0 @@
-package main
-
-import (
- "context"
- "testing"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/std"
- "github.com/gnolang/gno/tm2/pkg/testutils"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestGenesis_Balances_Remove(t *testing.T) {
- t.Parallel()
-
- t.Run("invalid genesis", func(t *testing.T) {
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "balances",
- "remove",
- "--genesis-path",
- "dummy-path",
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error())
- })
-
- t.Run("genesis app state not set", func(t *testing.T) {
- t.Parallel()
-
- dummyKey := getDummyKey(t)
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- genesis.AppState = nil // not set
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "balances",
- "remove",
- "--genesis-path",
- tempGenesis.Name(),
- "--address",
- dummyKey.Address().String(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.ErrorContains(t, cmdErr, errAppStateNotSet.Error())
- })
-
- t.Run("address is present", func(t *testing.T) {
- t.Parallel()
-
- dummyKey := getDummyKey(t)
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- state := gnoland.GnoGenesisState{
- // Set an initial balance value
- Balances: []gnoland.Balance{
- {
- Address: dummyKey.Address(),
- Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 100)),
- },
- },
- }
- genesis.AppState = state
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "balances",
- "remove",
- "--genesis-path",
- tempGenesis.Name(),
- "--address",
- dummyKey.Address().String(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Validate the genesis was updated
- genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name())
- require.NoError(t, loadErr)
-
- require.NotNil(t, genesis.AppState)
-
- state, ok := genesis.AppState.(gnoland.GnoGenesisState)
- require.True(t, ok)
-
- assert.Len(t, state.Balances, 0)
- })
-
- t.Run("address not present", func(t *testing.T) {
- t.Parallel()
-
- dummyKey := getDummyKey(t)
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- state := gnoland.GnoGenesisState{
- Balances: []gnoland.Balance{}, // Empty initial balance
- }
- genesis.AppState = state
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "balances",
- "remove",
- "--genesis-path",
- tempGenesis.Name(),
- "--address",
- dummyKey.Address().String(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.ErrorContains(t, cmdErr, errBalanceNotFound.Error())
- })
-}
diff --git a/gno.land/cmd/gnoland/genesis_generate.go b/gno.land/cmd/gnoland/genesis_generate.go
deleted file mode 100644
index 751ac14ae62..00000000000
--- a/gno.land/cmd/gnoland/genesis_generate.go
+++ /dev/null
@@ -1,153 +0,0 @@
-package main
-
-import (
- "context"
- "flag"
- "fmt"
- "time"
-
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
-)
-
-var defaultChainID = "dev"
-
-type generateCfg struct {
- outputPath string
- chainID string
- genesisTime int64
- blockMaxTxBytes int64
- blockMaxDataBytes int64
- blockMaxGas int64
- blockTimeIota int64
-}
-
-// newGenerateCmd creates the genesis generate subcommand
-func newGenerateCmd(io commands.IO) *commands.Command {
- cfg := &generateCfg{}
-
- return commands.NewCommand(
- commands.Metadata{
- Name: "generate",
- ShortUsage: "generate [flags]",
- ShortHelp: "generates a fresh genesis.json",
- LongHelp: "Generates a node's genesis.json based on specified parameters",
- },
- cfg,
- func(_ context.Context, _ []string) error {
- return execGenerate(cfg, io)
- },
- )
-}
-
-func (c *generateCfg) RegisterFlags(fs *flag.FlagSet) {
- fs.StringVar(
- &c.outputPath,
- "output-path",
- "./genesis.json",
- "the output path for the genesis.json",
- )
-
- fs.Int64Var(
- &c.genesisTime,
- "genesis-time",
- time.Now().Unix(),
- "the genesis creation time. Defaults to current time",
- )
-
- fs.StringVar(
- &c.chainID,
- "chain-id",
- defaultChainID,
- "the ID of the chain",
- )
-
- fs.Int64Var(
- &c.blockMaxTxBytes,
- "block-max-tx-bytes",
- types.MaxBlockTxBytes,
- "the max size of the block transaction",
- )
-
- fs.Int64Var(
- &c.blockMaxDataBytes,
- "block-max-data-bytes",
- types.MaxBlockDataBytes,
- "the max size of the block data",
- )
-
- fs.Int64Var(
- &c.blockMaxGas,
- "block-max-gas",
- types.MaxBlockMaxGas,
- "the max gas limit for the block",
- )
-
- fs.Int64Var(
- &c.blockTimeIota,
- "block-time-iota",
- types.BlockTimeIotaMS,
- "the block time iota (in ms)",
- )
-}
-
-func execGenerate(cfg *generateCfg, io commands.IO) error {
- // Start with the default configuration
- genesis := getDefaultGenesis()
-
- // Set the genesis time
- if cfg.genesisTime > 0 {
- genesis.GenesisTime = time.Unix(cfg.genesisTime, 0)
- }
-
- // Set the chain ID
- if cfg.chainID != "" {
- genesis.ChainID = cfg.chainID
- }
-
- // Set the max tx bytes
- if cfg.blockMaxTxBytes > 0 {
- genesis.ConsensusParams.Block.MaxTxBytes = cfg.blockMaxTxBytes
- }
-
- // Set the max data bytes
- if cfg.blockMaxDataBytes > 0 {
- genesis.ConsensusParams.Block.MaxDataBytes = cfg.blockMaxDataBytes
- }
-
- // Set the max block gas
- if cfg.blockMaxGas > 0 {
- genesis.ConsensusParams.Block.MaxGas = cfg.blockMaxGas
- }
-
- // Set the block time IOTA
- if cfg.blockTimeIota > 0 {
- genesis.ConsensusParams.Block.TimeIotaMS = cfg.blockTimeIota
- }
-
- // Validate the genesis
- if validateErr := genesis.ValidateAndComplete(); validateErr != nil {
- return fmt.Errorf("unable to validate genesis, %w", validateErr)
- }
-
- // Save the genesis file to disk
- if saveErr := genesis.SaveAs(cfg.outputPath); saveErr != nil {
- return fmt.Errorf("unable to save genesis, %w", saveErr)
- }
-
- io.Printfln("Genesis successfully generated at %s\n", cfg.outputPath)
-
- // Log the empty validator set warning
- io.Printfln("WARN: Genesis is generated with an empty validator set")
-
- return nil
-}
-
-// getDefaultGenesis returns the default genesis config
-func getDefaultGenesis() *types.GenesisDoc {
- return &types.GenesisDoc{
- GenesisTime: time.Now(),
- ChainID: defaultChainID,
- ConsensusParams: types.DefaultConsensusParams(),
- }
-}
diff --git a/gno.land/cmd/gnoland/genesis_generate_test.go b/gno.land/cmd/gnoland/genesis_generate_test.go
deleted file mode 100644
index f078a161662..00000000000
--- a/gno.land/cmd/gnoland/genesis_generate_test.go
+++ /dev/null
@@ -1,252 +0,0 @@
-package main
-
-import (
- "context"
- "fmt"
- "path/filepath"
- "testing"
-
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/testutils"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestGenesis_Generate(t *testing.T) {
- t.Parallel()
-
- t.Run("default genesis", func(t *testing.T) {
- t.Parallel()
-
- tempDir, cleanup := testutils.NewTestCaseDir(t)
- t.Cleanup(cleanup)
-
- genesisPath := filepath.Join(tempDir, "genesis.json")
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "generate",
- "--output-path",
- genesisPath,
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Load the genesis
- genesis, readErr := types.GenesisDocFromFile(genesisPath)
- require.NoError(t, readErr)
-
- // Make sure the default configuration is set
- defaultGenesis := getDefaultGenesis()
- defaultGenesis.GenesisTime = genesis.GenesisTime
-
- assert.Equal(t, defaultGenesis, genesis)
- })
-
- t.Run("set chain ID", func(t *testing.T) {
- t.Parallel()
-
- chainID := "example-chain-ID"
-
- tempDir, cleanup := testutils.NewTestCaseDir(t)
- t.Cleanup(cleanup)
-
- genesisPath := filepath.Join(tempDir, "genesis.json")
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "generate",
- "--chain-id",
- chainID,
- "--output-path",
- genesisPath,
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Load the genesis
- genesis, readErr := types.GenesisDocFromFile(genesisPath)
- require.NoError(t, readErr)
-
- assert.Equal(t, genesis.ChainID, chainID)
- })
-
- t.Run("set block max tx bytes", func(t *testing.T) {
- t.Parallel()
-
- blockMaxTxBytes := int64(100)
-
- tempDir, cleanup := testutils.NewTestCaseDir(t)
- t.Cleanup(cleanup)
-
- genesisPath := filepath.Join(tempDir, "genesis.json")
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "generate",
- "--block-max-tx-bytes",
- fmt.Sprintf("%d", blockMaxTxBytes),
- "--output-path",
- genesisPath,
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Load the genesis
- genesis, readErr := types.GenesisDocFromFile(genesisPath)
- require.NoError(t, readErr)
-
- assert.Equal(
- t,
- genesis.ConsensusParams.Block.MaxTxBytes,
- blockMaxTxBytes,
- )
- })
-
- t.Run("set block max data bytes", func(t *testing.T) {
- t.Parallel()
-
- blockMaxDataBytes := int64(100)
-
- tempDir, cleanup := testutils.NewTestCaseDir(t)
- t.Cleanup(cleanup)
-
- genesisPath := filepath.Join(tempDir, "genesis.json")
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "generate",
- "--block-max-data-bytes",
- fmt.Sprintf("%d", blockMaxDataBytes),
- "--output-path",
- genesisPath,
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Load the genesis
- genesis, readErr := types.GenesisDocFromFile(genesisPath)
- require.NoError(t, readErr)
-
- assert.Equal(
- t,
- genesis.ConsensusParams.Block.MaxDataBytes,
- blockMaxDataBytes,
- )
- })
-
- t.Run("set block max gas", func(t *testing.T) {
- t.Parallel()
-
- blockMaxGas := int64(100)
-
- tempDir, cleanup := testutils.NewTestCaseDir(t)
- t.Cleanup(cleanup)
-
- genesisPath := filepath.Join(tempDir, "genesis.json")
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "generate",
- "--block-max-gas",
- fmt.Sprintf("%d", blockMaxGas),
- "--output-path",
- genesisPath,
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Load the genesis
- genesis, readErr := types.GenesisDocFromFile(genesisPath)
- require.NoError(t, readErr)
-
- assert.Equal(
- t,
- genesis.ConsensusParams.Block.MaxGas,
- blockMaxGas,
- )
- })
-
- t.Run("set block time iota", func(t *testing.T) {
- t.Parallel()
-
- blockTimeIota := int64(10)
-
- tempDir, cleanup := testutils.NewTestCaseDir(t)
- t.Cleanup(cleanup)
-
- genesisPath := filepath.Join(tempDir, "genesis.json")
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "generate",
- "--block-time-iota",
- fmt.Sprintf("%d", blockTimeIota),
- "--output-path",
- genesisPath,
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Load the genesis
- genesis, readErr := types.GenesisDocFromFile(genesisPath)
- require.NoError(t, readErr)
-
- assert.Equal(
- t,
- genesis.ConsensusParams.Block.TimeIotaMS,
- blockTimeIota,
- )
- })
-
- t.Run("invalid genesis config (chain ID)", func(t *testing.T) {
- t.Parallel()
-
- invalidChainID := "thischainidisunusuallylongsoitwillcausethetesttofail"
-
- tempDir, cleanup := testutils.NewTestCaseDir(t)
- t.Cleanup(cleanup)
-
- genesisPath := filepath.Join(tempDir, "genesis.json")
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "generate",
- "--chain-id",
- invalidChainID,
- "--output-path",
- genesisPath,
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.Error(t, cmdErr)
- })
-}
diff --git a/gno.land/cmd/gnoland/genesis_txs.go b/gno.land/cmd/gnoland/genesis_txs.go
deleted file mode 100644
index 46b8d1bd29c..00000000000
--- a/gno.land/cmd/gnoland/genesis_txs.go
+++ /dev/null
@@ -1,76 +0,0 @@
-package main
-
-import (
- "errors"
- "flag"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/std"
-)
-
-type txsCfg struct {
- commonCfg
-}
-
-var errInvalidGenesisStateType = errors.New("invalid genesis state type")
-
-// newTxsCmd creates the genesis txs subcommand
-func newTxsCmd(io commands.IO) *commands.Command {
- cfg := &txsCfg{}
-
- cmd := commands.NewCommand(
- commands.Metadata{
- Name: "txs",
- ShortUsage: "txs [flags]",
- ShortHelp: "manages the initial genesis transactions",
- LongHelp: "Manages genesis transactions through input files",
- },
- cfg,
- commands.HelpExec,
- )
-
- cmd.AddSubCommands(
- newTxsAddCmd(cfg, io),
- newTxsRemoveCmd(cfg, io),
- newTxsExportCmd(cfg, io),
- newTxsListCmd(cfg, io),
- )
-
- return cmd
-}
-
-func (c *txsCfg) RegisterFlags(fs *flag.FlagSet) {
- c.commonCfg.RegisterFlags(fs)
-}
-
-// appendGenesisTxs saves the given transactions to the genesis doc
-func appendGenesisTxs(genesis *types.GenesisDoc, txs []std.Tx) error {
- // Initialize the app state if it's not present
- if genesis.AppState == nil {
- genesis.AppState = gnoland.GnoGenesisState{}
- }
-
- // Make sure the app state is the Gno genesis state
- state, ok := genesis.AppState.(gnoland.GnoGenesisState)
- if !ok {
- return errInvalidGenesisStateType
- }
-
- // Left merge the transactions
- fileTxStore := txStore(txs)
- genesisTxStore := txStore(state.Txs)
-
- // The genesis transactions have preference with the order
- // in the genesis.json
- if err := genesisTxStore.leftMerge(fileTxStore); err != nil {
- return err
- }
-
- // Save the state
- state.Txs = genesisTxStore
- genesis.AppState = state
-
- return nil
-}
diff --git a/gno.land/cmd/gnoland/genesis_txs_add.go b/gno.land/cmd/gnoland/genesis_txs_add.go
deleted file mode 100644
index 7e7fd25b21e..00000000000
--- a/gno.land/cmd/gnoland/genesis_txs_add.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package main
-
-import (
- "github.com/gnolang/gno/tm2/pkg/commands"
-)
-
-// newTxsAddCmd creates the genesis txs add subcommand
-func newTxsAddCmd(txsCfg *txsCfg, io commands.IO) *commands.Command {
- cmd := commands.NewCommand(
- commands.Metadata{
- Name: "add",
- ShortUsage: "txs add [flags] [...]",
- ShortHelp: "adds transactions into the genesis.json",
- LongHelp: "Adds initial transactions to the genesis.json",
- },
- commands.NewEmptyConfig(),
- commands.HelpExec,
- )
-
- cmd.AddSubCommands(
- newTxsAddSheetCmd(txsCfg, io),
- newTxsAddPackagesCmd(txsCfg, io),
- )
-
- return cmd
-}
diff --git a/gno.land/cmd/gnoland/genesis_txs_add_packages.go b/gno.land/cmd/gnoland/genesis_txs_add_packages.go
deleted file mode 100644
index 56d165c070b..00000000000
--- a/gno.land/cmd/gnoland/genesis_txs_add_packages.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package main
-
-import (
- "context"
- "errors"
- "fmt"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/crypto"
- "github.com/gnolang/gno/tm2/pkg/std"
-)
-
-var errInvalidPackageDir = errors.New("invalid package directory")
-
-var (
- genesisDeployAddress = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1
- genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000)))
-)
-
-// newTxsAddPackagesCmd creates the genesis txs add packages subcommand
-func newTxsAddPackagesCmd(txsCfg *txsCfg, io commands.IO) *commands.Command {
- return commands.NewCommand(
- commands.Metadata{
- Name: "packages",
- ShortUsage: "txs add packages ",
- ShortHelp: "imports transactions from the given packages into the genesis.json",
- LongHelp: "Imports the transactions from a given package directory recursively to the genesis.json",
- },
- commands.NewEmptyConfig(),
- func(_ context.Context, args []string) error {
- return execTxsAddPackages(txsCfg, io, args)
- },
- )
-}
-
-func execTxsAddPackages(
- cfg *txsCfg,
- io commands.IO,
- args []string,
-) error {
- // Load the genesis
- genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath)
- if loadErr != nil {
- return fmt.Errorf("unable to load genesis, %w", loadErr)
- }
-
- // Make sure the package dir is set
- if len(args) == 0 {
- return errInvalidPackageDir
- }
-
- parsedTxs := make([]std.Tx, 0)
- for _, path := range args {
- // Generate transactions from the packages (recursively)
- txs, err := gnoland.LoadPackagesFromDir(path, genesisDeployAddress, genesisDeployFee)
- if err != nil {
- return fmt.Errorf("unable to load txs from directory, %w", err)
- }
-
- parsedTxs = append(parsedTxs, txs...)
- }
-
- // Save the txs to the genesis.json
- if err := appendGenesisTxs(genesis, parsedTxs); err != nil {
- return fmt.Errorf("unable to append genesis transactions, %w", err)
- }
-
- // Save the updated genesis
- if err := genesis.SaveAs(cfg.genesisPath); err != nil {
- return fmt.Errorf("unable to save genesis.json, %w", err)
- }
-
- io.Printfln(
- "Saved %d transactions to genesis.json",
- len(parsedTxs),
- )
-
- return nil
-}
diff --git a/gno.land/cmd/gnoland/genesis_txs_add_packages_test.go b/gno.land/cmd/gnoland/genesis_txs_add_packages_test.go
deleted file mode 100644
index 20c4f84c9ed..00000000000
--- a/gno.land/cmd/gnoland/genesis_txs_add_packages_test.go
+++ /dev/null
@@ -1,133 +0,0 @@
-package main
-
-import (
- "context"
- "fmt"
- "os"
- "path/filepath"
- "testing"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/testutils"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestGenesis_Txs_Add_Packages(t *testing.T) {
- t.Parallel()
-
- t.Run("invalid genesis file", func(t *testing.T) {
- t.Parallel()
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "add",
- "packages",
- "--genesis-path",
- "dummy-path",
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error())
- })
-
- t.Run("invalid package dir", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "add",
- "packages",
- "--genesis-path",
- tempGenesis.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errInvalidPackageDir.Error())
- })
-
- t.Run("valid package", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Prepare the package
- var (
- packagePath = "gno.land/p/demo/cuttlas"
- dir = t.TempDir()
- )
-
- createFile := func(path, data string) {
- file, err := os.Create(path)
- require.NoError(t, err)
-
- _, err = file.WriteString(data)
- require.NoError(t, err)
- }
-
- // Create the gno.mod file
- createFile(
- filepath.Join(dir, "gno.mod"),
- fmt.Sprintf("module %s\n", packagePath),
- )
-
- // Create a simple main.gno
- createFile(
- filepath.Join(dir, "main.gno"),
- "package cuttlas\n\nfunc Example() string {\nreturn \"Manos arriba!\"\n}",
- )
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "add",
- "packages",
- "--genesis-path",
- tempGenesis.Name(),
- dir,
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Validate the transactions were written down
- updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name())
- require.NoError(t, err)
- require.NotNil(t, updatedGenesis.AppState)
-
- // Fetch the state
- state := updatedGenesis.AppState.(gnoland.GnoGenesisState)
-
- require.Equal(t, 1, len(state.Txs))
- require.Equal(t, 1, len(state.Txs[0].Msgs))
-
- msgAddPkg, ok := state.Txs[0].Msgs[0].(vmm.MsgAddPackage)
- require.True(t, ok)
-
- assert.Equal(t, packagePath, msgAddPkg.Package.Path)
- })
-}
diff --git a/gno.land/cmd/gnoland/genesis_txs_add_sheet.go b/gno.land/cmd/gnoland/genesis_txs_add_sheet.go
deleted file mode 100644
index 261a050029c..00000000000
--- a/gno.land/cmd/gnoland/genesis_txs_add_sheet.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package main
-
-import (
- "context"
- "errors"
- "fmt"
- "os"
-
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/std"
-)
-
-var (
- errInvalidTxsFile = errors.New("unable to open transactions file")
- errNoTxsFileSpecified = errors.New("no txs file specified")
-)
-
-// newTxsAddSheetCmd creates the genesis txs add sheet subcommand
-func newTxsAddSheetCmd(txsCfg *txsCfg, io commands.IO) *commands.Command {
- return commands.NewCommand(
- commands.Metadata{
- Name: "sheets",
- ShortUsage: "txs add sheets ",
- ShortHelp: "imports transactions from the given sheets into the genesis.json",
- LongHelp: "Imports the transactions from a given transactions sheet to the genesis.json",
- },
- commands.NewEmptyConfig(),
- func(ctx context.Context, args []string) error {
- return execTxsAddSheet(ctx, txsCfg, io, args)
- },
- )
-}
-
-func execTxsAddSheet(
- ctx context.Context,
- cfg *txsCfg,
- io commands.IO,
- args []string,
-) error {
- // Load the genesis
- genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath)
- if loadErr != nil {
- return fmt.Errorf("unable to load genesis, %w", loadErr)
- }
-
- // Open the transactions files
- if len(args) == 0 {
- return errNoTxsFileSpecified
- }
-
- parsedTxs := make([]std.Tx, 0)
- for _, file := range args {
- file, loadErr := os.Open(file)
- if loadErr != nil {
- return fmt.Errorf("%w, %w", errInvalidTxsFile, loadErr)
- }
-
- txs, err := std.ParseTxs(ctx, file)
- if err != nil {
- return fmt.Errorf("unable to parse file, %w", err)
- }
-
- if err = file.Close(); err != nil {
- return fmt.Errorf("unable to gracefully close file, %w", err)
- }
-
- parsedTxs = append(parsedTxs, txs...)
- }
-
- // Save the txs to the genesis.json
- if err := appendGenesisTxs(genesis, parsedTxs); err != nil {
- return fmt.Errorf("unable to append genesis transactions, %w", err)
- }
-
- // Save the updated genesis
- if err := genesis.SaveAs(cfg.genesisPath); err != nil {
- return fmt.Errorf("unable to save genesis.json, %w", err)
- }
-
- io.Printfln(
- "Saved %d transactions to genesis.json",
- len(parsedTxs),
- )
-
- return nil
-}
diff --git a/gno.land/cmd/gnoland/genesis_txs_add_sheet_test.go b/gno.land/cmd/gnoland/genesis_txs_add_sheet_test.go
deleted file mode 100644
index a70446cfe6c..00000000000
--- a/gno.land/cmd/gnoland/genesis_txs_add_sheet_test.go
+++ /dev/null
@@ -1,279 +0,0 @@
-package main
-
-import (
- "context"
- "fmt"
- "strings"
- "testing"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
- "github.com/gnolang/gno/tm2/pkg/amino"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/crypto"
- "github.com/gnolang/gno/tm2/pkg/sdk/bank"
- "github.com/gnolang/gno/tm2/pkg/std"
- "github.com/gnolang/gno/tm2/pkg/testutils"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-// generateDummyTxs generates dummy transactions
-func generateDummyTxs(t *testing.T, count int) []std.Tx {
- t.Helper()
-
- txs := make([]std.Tx, count)
-
- for i := 0; i < count; i++ {
- txs[i] = std.Tx{
- Msgs: []std.Msg{
- bank.MsgSend{
- FromAddress: crypto.Address{byte(i)},
- ToAddress: crypto.Address{byte((i + 1) % count)},
- Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 1)),
- },
- },
- Fee: std.Fee{
- GasWanted: 1,
- GasFee: std.NewCoin(ugnot.Denom, 1000000),
- },
- Memo: fmt.Sprintf("tx %d", i),
- }
- }
-
- return txs
-}
-
-// encodeDummyTxs encodes the transactions into amino JSON
-func encodeDummyTxs(t *testing.T, txs []std.Tx) []string {
- t.Helper()
-
- encodedTxs := make([]string, 0, len(txs))
-
- for _, tx := range txs {
- encodedTx, err := amino.MarshalJSON(tx)
- if err != nil {
- t.Fatalf("unable to marshal tx, %v", err)
- }
-
- encodedTxs = append(encodedTxs, string(encodedTx))
- }
-
- return encodedTxs
-}
-
-func TestGenesis_Txs_Add_Sheets(t *testing.T) {
- t.Parallel()
-
- t.Run("invalid genesis file", func(t *testing.T) {
- t.Parallel()
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "add",
- "sheets",
- "--genesis-path",
- "dummy-path",
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error())
- })
-
- t.Run("invalid txs file", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "add",
- "sheets",
- "--genesis-path",
- tempGenesis.Name(),
- "dummy-tx-file",
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errInvalidTxsFile.Error())
- })
-
- t.Run("no txs file", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "add",
- "sheets",
- "--genesis-path",
- tempGenesis.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errNoTxsFileSpecified.Error())
- })
-
- t.Run("malformed txs file", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "add",
- "sheets",
- "--genesis-path",
- tempGenesis.Name(),
- tempGenesis.Name(), // invalid txs file
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, "unable to parse file")
- })
-
- t.Run("valid txs file", func(t *testing.T) {
- t.Parallel()
-
- // Generate dummy txs
- txs := generateDummyTxs(t, 10)
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Prepare the transactions file
- txsFile, txsCleanup := testutils.NewTestFile(t)
- t.Cleanup(txsCleanup)
-
- _, err := txsFile.WriteString(
- strings.Join(
- encodeDummyTxs(t, txs),
- "\n",
- ),
- )
- require.NoError(t, err)
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "add",
- "sheets",
- "--genesis-path",
- tempGenesis.Name(),
- txsFile.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Validate the transactions were written down
- updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name())
- require.NoError(t, err)
- require.NotNil(t, updatedGenesis.AppState)
-
- // Fetch the state
- state := updatedGenesis.AppState.(gnoland.GnoGenesisState)
-
- assert.Len(t, state.Txs, len(txs))
-
- for index, tx := range state.Txs {
- assert.Equal(t, txs[index], tx)
- }
- })
-
- t.Run("existing genesis txs", func(t *testing.T) {
- t.Parallel()
-
- // Generate dummy txs
- txs := generateDummyTxs(t, 10)
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- genesisState := gnoland.GnoGenesisState{
- Txs: txs[0 : len(txs)/2],
- }
-
- genesis.AppState = genesisState
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Prepare the transactions file
- txsFile, txsCleanup := testutils.NewTestFile(t)
- t.Cleanup(txsCleanup)
-
- _, err := txsFile.WriteString(
- strings.Join(
- encodeDummyTxs(t, txs),
- "\n",
- ),
- )
- require.NoError(t, err)
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "add",
- "sheets",
- "--genesis-path",
- tempGenesis.Name(),
- txsFile.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Validate the transactions were written down
- updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name())
- require.NoError(t, err)
- require.NotNil(t, updatedGenesis.AppState)
-
- // Fetch the state
- state := updatedGenesis.AppState.(gnoland.GnoGenesisState)
-
- assert.Len(t, state.Txs, len(txs))
-
- for index, tx := range state.Txs {
- assert.Equal(t, txs[index], tx)
- }
- })
-}
diff --git a/gno.land/cmd/gnoland/genesis_txs_export.go b/gno.land/cmd/gnoland/genesis_txs_export.go
deleted file mode 100644
index bf54236b31f..00000000000
--- a/gno.land/cmd/gnoland/genesis_txs_export.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package main
-
-import (
- "context"
- "errors"
- "fmt"
- "os"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/tm2/pkg/amino"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
-)
-
-var errNoOutputFile = errors.New("no output file path specified")
-
-// newTxsExportCmd creates the genesis txs export subcommand
-func newTxsExportCmd(txsCfg *txsCfg, io commands.IO) *commands.Command {
- return commands.NewCommand(
- commands.Metadata{
- Name: "export",
- ShortUsage: "txs export [flags] ",
- ShortHelp: "exports the transactions from the genesis.json",
- LongHelp: "Exports the transactions from the genesis.json to an output file",
- },
- commands.NewEmptyConfig(),
- func(_ context.Context, args []string) error {
- return execTxsExport(txsCfg, io, args)
- },
- )
-}
-
-func execTxsExport(cfg *txsCfg, io commands.IO, args []string) error {
- // Load the genesis
- genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath)
- if loadErr != nil {
- return fmt.Errorf("unable to load genesis, %w", loadErr)
- }
-
- // Load the genesis state
- if genesis.AppState == nil {
- return errAppStateNotSet
- }
-
- state := genesis.AppState.(gnoland.GnoGenesisState)
- if len(state.Txs) == 0 {
- io.Println("No genesis transactions to export")
-
- return nil
- }
-
- // Make sure the output file path is specified
- if len(args) == 0 {
- return errNoOutputFile
- }
-
- // Open output file
- outputFile, err := os.OpenFile(
- args[0],
- os.O_RDWR|os.O_CREATE|os.O_APPEND,
- 0o755,
- )
- if err != nil {
- return fmt.Errorf("unable to create output file, %w", err)
- }
-
- // Save the transactions
- for _, tx := range state.Txs {
- // Marshal tx individual tx into JSON
- jsonData, err := amino.MarshalJSON(tx)
- if err != nil {
- return fmt.Errorf("unable to marshal JSON data, %w", err)
- }
-
- // Write the JSON data as a line to the file
- if _, err = outputFile.Write(jsonData); err != nil {
- return fmt.Errorf("unable to write to output, %w", err)
- }
-
- // Write a newline character to separate JSON objects
- if _, err = outputFile.WriteString("\n"); err != nil {
- return fmt.Errorf("unable to write newline output, %w", err)
- }
- }
-
- io.Printfln(
- "Exported %d transactions",
- len(state.Txs),
- )
-
- return nil
-}
diff --git a/gno.land/cmd/gnoland/genesis_txs_export_test.go b/gno.land/cmd/gnoland/genesis_txs_export_test.go
deleted file mode 100644
index 9927f671efb..00000000000
--- a/gno.land/cmd/gnoland/genesis_txs_export_test.go
+++ /dev/null
@@ -1,144 +0,0 @@
-package main
-
-import (
- "bufio"
- "context"
- "testing"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/tm2/pkg/amino"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/std"
- "github.com/gnolang/gno/tm2/pkg/testutils"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestGenesis_Txs_Export(t *testing.T) {
- t.Parallel()
-
- t.Run("invalid genesis file", func(t *testing.T) {
- t.Parallel()
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "export",
- "--genesis-path",
- "dummy-path",
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error())
- })
-
- t.Run("invalid genesis app state", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- genesis.AppState = nil // no app state
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "export",
- "--genesis-path",
- tempGenesis.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errAppStateNotSet.Error())
- })
-
- t.Run("no output file specified", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- genesis.AppState = gnoland.GnoGenesisState{
- Txs: generateDummyTxs(t, 1),
- }
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "export",
- "--genesis-path",
- tempGenesis.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errNoOutputFile.Error())
- })
-
- t.Run("valid txs export", func(t *testing.T) {
- t.Parallel()
-
- // Generate dummy txs
- txs := generateDummyTxs(t, 10)
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- genesis.AppState = gnoland.GnoGenesisState{
- Txs: txs,
- }
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Prepare the output file
- outputFile, outputCleanup := testutils.NewTestFile(t)
- t.Cleanup(outputCleanup)
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "export",
- "--genesis-path",
- tempGenesis.Name(),
- outputFile.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Validate the transactions were written down
- scanner := bufio.NewScanner(outputFile)
-
- outputTxs := make([]std.Tx, 0)
- for scanner.Scan() {
- var tx std.Tx
-
- require.NoError(t, amino.UnmarshalJSON(scanner.Bytes(), &tx))
-
- outputTxs = append(outputTxs, tx)
- }
-
- require.NoError(t, scanner.Err())
-
- assert.Len(t, outputTxs, len(txs))
-
- for index, tx := range outputTxs {
- assert.Equal(t, txs[index], tx)
- }
- })
-}
diff --git a/gno.land/cmd/gnoland/genesis_txs_list.go b/gno.land/cmd/gnoland/genesis_txs_list.go
deleted file mode 100644
index c68fbc30803..00000000000
--- a/gno.land/cmd/gnoland/genesis_txs_list.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package main
-
-import (
- "bytes"
- "context"
- "errors"
- "fmt"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/tm2/pkg/amino"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
-)
-
-var ErrWrongGenesisType = errors.New("genesis state is not using the correct Gno Genesis type")
-
-// newTxsListCmd list all transactions on the specified genesis file
-func newTxsListCmd(txsCfg *txsCfg, io commands.IO) *commands.Command {
- cmd := commands.NewCommand(
- commands.Metadata{
- Name: "list",
- ShortUsage: "txs list [flags] [...]",
- ShortHelp: "lists transactions existing on genesis.json",
- LongHelp: "Lists transactions existing on genesis.json",
- },
- commands.NewEmptyConfig(),
- func(ctx context.Context, args []string) error {
- return execTxsListCmd(io, txsCfg)
- },
- )
-
- return cmd
-}
-
-func execTxsListCmd(io commands.IO, cfg *txsCfg) error {
- genesis, err := types.GenesisDocFromFile(cfg.genesisPath)
- if err != nil {
- return fmt.Errorf("%w, %w", errUnableToLoadGenesis, err)
- }
-
- gs, ok := genesis.AppState.(gnoland.GnoGenesisState)
- if !ok {
- return ErrWrongGenesisType
- }
-
- b, err := amino.MarshalJSONIndent(gs.Txs, "", " ")
- if err != nil {
- return errors.New("error marshalling data to amino JSON")
- }
-
- buf := bytes.NewBuffer(b)
- _, err = buf.WriteTo(io.Out())
-
- return err
-}
diff --git a/gno.land/cmd/gnoland/genesis_txs_list_test.go b/gno.land/cmd/gnoland/genesis_txs_list_test.go
deleted file mode 100644
index d18c2f4d641..00000000000
--- a/gno.land/cmd/gnoland/genesis_txs_list_test.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package main
-
-import (
- "bytes"
- "context"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/testutils"
-)
-
-func TestGenesis_List_All(t *testing.T) {
- t.Parallel()
-
- t.Run("invalid genesis path", func(t *testing.T) {
- t.Parallel()
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "list",
- "--genesis-path",
- "",
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorIs(t, cmdErr, errUnableToLoadGenesis)
- })
-
- t.Run("list all txs", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- // Generate dummy txs
- txs := generateDummyTxs(t, 10)
-
- genesis := getDefaultGenesis()
- genesis.AppState = gnoland.GnoGenesisState{
- Txs: txs,
- }
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- cio := commands.NewTestIO()
- buf := bytes.NewBuffer(nil)
- cio.SetOut(commands.WriteNopCloser(buf))
-
- cmd := newRootCmd(cio)
- args := []string{
- "genesis",
- "txs",
- "list",
- "--genesis-path",
- tempGenesis.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- require.Len(t, buf.String(), 4442)
- })
-}
diff --git a/gno.land/cmd/gnoland/genesis_txs_remove.go b/gno.land/cmd/gnoland/genesis_txs_remove.go
deleted file mode 100644
index 49c650f4670..00000000000
--- a/gno.land/cmd/gnoland/genesis_txs_remove.go
+++ /dev/null
@@ -1,108 +0,0 @@
-package main
-
-import (
- "context"
- "errors"
- "fmt"
- "strings"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/tm2/pkg/amino"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/std"
-)
-
-var (
- errAppStateNotSet = errors.New("genesis app state not set")
- errNoTxHashSpecified = errors.New("no transaction hashes specified")
- errTxNotFound = errors.New("transaction not present in genesis.json")
-)
-
-// newTxsRemoveCmd creates the genesis txs remove subcommand
-func newTxsRemoveCmd(txsCfg *txsCfg, io commands.IO) *commands.Command {
- return commands.NewCommand(
- commands.Metadata{
- Name: "remove",
- ShortUsage: "txs remove ",
- ShortHelp: "removes the transactions from the genesis.json",
- LongHelp: "Removes the transactions using the transaction hash",
- },
- commands.NewEmptyConfig(),
- func(_ context.Context, args []string) error {
- return execTxsRemove(txsCfg, io, args)
- },
- )
-}
-
-func execTxsRemove(cfg *txsCfg, io commands.IO, args []string) error {
- // Load the genesis
- genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath)
- if loadErr != nil {
- return fmt.Errorf("unable to load genesis, %w", loadErr)
- }
-
- // Check if the genesis state is set at all
- if genesis.AppState == nil {
- return errAppStateNotSet
- }
-
- // Make sure the transaction hashes are set
- if len(args) == 0 {
- return errNoTxHashSpecified
- }
-
- state := genesis.AppState.(gnoland.GnoGenesisState)
-
- for _, inputHash := range args {
- index := -1
-
- for indx, tx := range state.Txs {
- // Find the hash of the transaction
- hash, err := getTxHash(tx)
- if err != nil {
- return fmt.Errorf("unable to generate tx hash, %w", err)
- }
-
- // Check if the hashes match
- if strings.ToLower(hash) == strings.ToLower(inputHash) {
- index = indx
-
- break
- }
- }
-
- if index < 0 {
- return errTxNotFound
- }
-
- state.Txs = append(state.Txs[:index], state.Txs[index+1:]...)
-
- io.Printfln(
- "Transaction %s removed from genesis.json",
- inputHash,
- )
- }
-
- genesis.AppState = state
-
- // Save the updated genesis
- if err := genesis.SaveAs(cfg.genesisPath); err != nil {
- return fmt.Errorf("unable to save genesis.json, %w", err)
- }
-
- return nil
-}
-
-// getTxHash returns the hex hash representation of
-// the transaction (Amino encoded)
-func getTxHash(tx std.Tx) (string, error) {
- encodedTx, err := amino.Marshal(tx)
- if err != nil {
- return "", fmt.Errorf("unable to marshal transaction, %w", err)
- }
-
- txHash := types.Tx(encodedTx).Hash()
-
- return fmt.Sprintf("%X", txHash), nil
-}
diff --git a/gno.land/cmd/gnoland/genesis_txs_remove_test.go b/gno.land/cmd/gnoland/genesis_txs_remove_test.go
deleted file mode 100644
index ff5af479449..00000000000
--- a/gno.land/cmd/gnoland/genesis_txs_remove_test.go
+++ /dev/null
@@ -1,140 +0,0 @@
-package main
-
-import (
- "context"
- "testing"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/testutils"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestGenesis_Txs_Remove(t *testing.T) {
- t.Parallel()
-
- t.Run("invalid genesis file", func(t *testing.T) {
- t.Parallel()
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "remove",
- "--genesis-path",
- "dummy-path",
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error())
- })
-
- t.Run("invalid genesis app state", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- genesis.AppState = nil // no app state
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "remove",
- "--genesis-path",
- tempGenesis.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errAppStateNotSet.Error())
- })
- t.Run("no transaction hash specified", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- // Generate dummy txs
- txs := generateDummyTxs(t, 10)
-
- genesis := getDefaultGenesis()
- genesis.AppState = gnoland.GnoGenesisState{
- Txs: txs,
- }
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "remove",
- "--genesis-path",
- tempGenesis.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errNoTxHashSpecified.Error())
- })
-
- t.Run("transaction removed", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- // Generate dummy txs
- txs := generateDummyTxs(t, 10)
-
- genesis := getDefaultGenesis()
- genesis.AppState = gnoland.GnoGenesisState{
- Txs: txs,
- }
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- txHash, err := getTxHash(txs[0])
- require.NoError(t, err)
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "txs",
- "remove",
- "--genesis-path",
- tempGenesis.Name(),
- txHash,
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
-
- // Validate the transaction was removed
- updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name())
- require.NoError(t, err)
- require.NotNil(t, updatedGenesis.AppState)
-
- // Fetch the state
- state := updatedGenesis.AppState.(gnoland.GnoGenesisState)
-
- assert.Len(t, state.Txs, len(txs)-1)
-
- for _, tx := range state.Txs {
- genesisTxHash, err := getTxHash(tx)
- require.NoError(t, err)
-
- assert.NotEqual(t, txHash, genesisTxHash)
- }
- })
-}
diff --git a/gno.land/cmd/gnoland/genesis_validator.go b/gno.land/cmd/gnoland/genesis_validator.go
deleted file mode 100644
index 91d3e4af7dd..00000000000
--- a/gno.land/cmd/gnoland/genesis_validator.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package main
-
-import (
- "flag"
-
- "github.com/gnolang/gno/tm2/pkg/commands"
-)
-
-type validatorCfg struct {
- commonCfg
-
- address string
-}
-
-// newValidatorCmd creates the genesis validator subcommand
-func newValidatorCmd(io commands.IO) *commands.Command {
- cfg := &validatorCfg{
- commonCfg: commonCfg{},
- }
-
- cmd := commands.NewCommand(
- commands.Metadata{
- Name: "validator",
- ShortUsage: "validator [flags]",
- ShortHelp: "validator set management in genesis.json",
- LongHelp: "Manipulates the genesis.json validator set",
- },
- cfg,
- commands.HelpExec,
- )
-
- cmd.AddSubCommands(
- newValidatorAddCmd(cfg, io),
- newValidatorRemoveCmd(cfg, io),
- )
-
- return cmd
-}
-
-func (c *validatorCfg) RegisterFlags(fs *flag.FlagSet) {
- c.commonCfg.RegisterFlags(fs)
-
- fs.StringVar(
- &c.address,
- "address",
- "",
- "the gno bech32 address of the validator",
- )
-}
diff --git a/gno.land/cmd/gnoland/genesis_validator_add.go b/gno.land/cmd/gnoland/genesis_validator_add.go
deleted file mode 100644
index 6c44ad93f89..00000000000
--- a/gno.land/cmd/gnoland/genesis_validator_add.go
+++ /dev/null
@@ -1,137 +0,0 @@
-package main
-
-import (
- "context"
- "errors"
- "flag"
- "fmt"
-
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/crypto"
- _ "github.com/gnolang/gno/tm2/pkg/crypto/keys"
-)
-
-var (
- errInvalidPower = errors.New("invalid validator power")
- errInvalidName = errors.New("invalid validator name")
- errPublicKeyAddressMismatch = errors.New("provided public key and address do not match")
- errAddressPresent = errors.New("validator with same address already present in genesis.json")
-)
-
-type validatorAddCfg struct {
- rootCfg *validatorCfg
-
- pubKey string
- name string
- power int64
-}
-
-// newValidatorAddCmd creates the genesis validator add subcommand
-func newValidatorAddCmd(validatorCfg *validatorCfg, io commands.IO) *commands.Command {
- cfg := &validatorAddCfg{
- rootCfg: validatorCfg,
- }
-
- return commands.NewCommand(
- commands.Metadata{
- Name: "add",
- ShortUsage: "validator add [flags]",
- ShortHelp: "adds a new validator to the genesis.json",
- },
- cfg,
- func(_ context.Context, _ []string) error {
- return execValidatorAdd(cfg, io)
- },
- )
-}
-
-func (c *validatorAddCfg) RegisterFlags(fs *flag.FlagSet) {
- fs.StringVar(
- &c.pubKey,
- "pub-key",
- "",
- "the bech32 string representation of the validator's public key",
- )
-
- fs.StringVar(
- &c.name,
- "name",
- "",
- "the name of the validator (must be unique)",
- )
-
- fs.Int64Var(
- &c.power,
- "power",
- 1,
- "the voting power of the validator (must be > 0)",
- )
-}
-
-func execValidatorAdd(cfg *validatorAddCfg, io commands.IO) error {
- // Load the genesis
- genesis, loadErr := types.GenesisDocFromFile(cfg.rootCfg.genesisPath)
- if loadErr != nil {
- return fmt.Errorf("unable to load genesis, %w", loadErr)
- }
-
- // Check the validator address
- address, err := crypto.AddressFromString(cfg.rootCfg.address)
- if err != nil {
- return fmt.Errorf("invalid validator address, %w", err)
- }
-
- // Check the voting power
- if cfg.power < 1 {
- return errInvalidPower
- }
-
- // Check the name
- if cfg.name == "" {
- return errInvalidName
- }
-
- // Check the public key
- pubKey, err := crypto.PubKeyFromBech32(cfg.pubKey)
- if err != nil {
- return fmt.Errorf("invalid validator public key, %w", err)
- }
-
- // Check the public key matches the address
- if pubKey.Address() != address {
- return errPublicKeyAddressMismatch
- }
-
- validator := types.GenesisValidator{
- Address: address,
- PubKey: pubKey,
- Power: cfg.power,
- Name: cfg.name,
- }
-
- // Check if the validator exists
- for _, genesisValidator := range genesis.Validators {
- // There is no need to check if the public keys match
- // since the address is derived from it, and the derivation
- // is checked already
- if validator.Address == genesisValidator.Address {
- return errAddressPresent
- }
- }
-
- // Add the validator
- genesis.Validators = append(genesis.Validators, validator)
-
- // Save the updated genesis
- if err := genesis.SaveAs(cfg.rootCfg.genesisPath); err != nil {
- return fmt.Errorf("unable to save genesis.json, %w", err)
- }
-
- io.Printfln(
- "Validator with address %s added to genesis file",
- cfg.rootCfg.address,
- )
-
- return nil
-}
diff --git a/gno.land/cmd/gnoland/genesis_validator_add_test.go b/gno.land/cmd/gnoland/genesis_validator_add_test.go
deleted file mode 100644
index 528255b3029..00000000000
--- a/gno.land/cmd/gnoland/genesis_validator_add_test.go
+++ /dev/null
@@ -1,301 +0,0 @@
-package main
-
-import (
- "context"
- "testing"
-
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/crypto"
- "github.com/gnolang/gno/tm2/pkg/crypto/bip39"
- "github.com/gnolang/gno/tm2/pkg/crypto/hd"
- "github.com/gnolang/gno/tm2/pkg/crypto/keys/client"
- "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1"
- "github.com/gnolang/gno/tm2/pkg/testutils"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-// getDummyKey generates a random public key,
-// and returns the key info
-func getDummyKey(t *testing.T) crypto.PubKey {
- t.Helper()
-
- mnemonic, err := client.GenerateMnemonic(256)
- require.NoError(t, err)
-
- seed := bip39.NewSeed(mnemonic, "")
-
- return generateKeyFromSeed(seed, 0).PubKey()
-}
-
-// generateKeyFromSeed generates a private key from
-// the provided seed and index
-func generateKeyFromSeed(seed []byte, index uint32) crypto.PrivKey {
- pathParams := hd.NewFundraiserParams(0, crypto.CoinType, index)
-
- masterPriv, ch := hd.ComputeMastersFromSeed(seed)
-
- //nolint:errcheck // This derivation can never error out, since the path params
- // are always going to be valid
- derivedPriv, _ := hd.DerivePrivateKeyForPath(masterPriv, ch, pathParams.String())
-
- return secp256k1.PrivKeySecp256k1(derivedPriv)
-}
-
-// getDummyKeys generates random keys for testing
-func getDummyKeys(t *testing.T, count int) []crypto.PubKey {
- t.Helper()
-
- dummyKeys := make([]crypto.PubKey, count)
-
- for i := 0; i < count; i++ {
- dummyKeys[i] = getDummyKey(t)
- }
-
- return dummyKeys
-}
-
-func TestGenesis_Validator_Add(t *testing.T) {
- t.Parallel()
-
- t.Run("invalid genesis file", func(t *testing.T) {
- t.Parallel()
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "validator",
- "add",
- "--genesis-path",
- "dummy-path",
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error())
- })
-
- t.Run("invalid validator address", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "validator",
- "add",
- "--genesis-path",
- tempGenesis.Name(),
- "--address",
- "dummyaddress",
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, "invalid validator address")
- })
-
- t.Run("invalid voting power", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- key := getDummyKey(t)
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "validator",
- "add",
- "--genesis-path",
- tempGenesis.Name(),
- "--address",
- key.Address().String(),
- "--power",
- "-1", // invalid voting power
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorIs(t, cmdErr, errInvalidPower)
- })
-
- t.Run("invalid validator name", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- key := getDummyKey(t)
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "validator",
- "add",
- "--genesis-path",
- tempGenesis.Name(),
- "--address",
- key.Address().String(),
- "--name",
- "", // invalid validator name
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errInvalidName.Error())
- })
-
- t.Run("invalid public key", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- key := getDummyKey(t)
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "validator",
- "add",
- "--genesis-path",
- tempGenesis.Name(),
- "--address",
- key.Address().String(),
- "--name",
- "example",
- "--pub-key",
- "invalidkey", // invalid pub key
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, "invalid validator public key")
- })
-
- t.Run("public key address mismatch", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- dummyKeys := getDummyKeys(t, 2)
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "validator",
- "add",
- "--genesis-path",
- tempGenesis.Name(),
- "--address",
- dummyKeys[0].Address().String(),
- "--name",
- "example",
- "--pub-key",
- crypto.PubKeyToBech32(dummyKeys[1]), // another key
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errPublicKeyAddressMismatch.Error())
- })
-
- t.Run("validator with same address exists", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- dummyKeys := getDummyKeys(t, 2)
- genesis := getDefaultGenesis()
-
- // Set an existing validator
- genesis.Validators = append(genesis.Validators, types.GenesisValidator{
- Address: dummyKeys[0].Address(),
- PubKey: dummyKeys[0],
- Power: 1,
- Name: "example",
- })
-
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "validator",
- "add",
- "--genesis-path",
- tempGenesis.Name(),
- "--address",
- dummyKeys[0].Address().String(),
- "--name",
- "example",
- "--pub-key",
- crypto.PubKeyToBech32(dummyKeys[0]), // another key
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errAddressPresent.Error())
- })
-
- t.Run("valid genesis validator", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- key := getDummyKey(t)
- genesis := getDefaultGenesis()
-
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "validator",
- "add",
- "--genesis-path",
- tempGenesis.Name(),
- "--address",
- key.Address().String(),
- "--name",
- "example",
- "--pub-key",
- crypto.PubKeyToBech32(key), // another key
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
- })
-}
diff --git a/gno.land/cmd/gnoland/genesis_validator_remove.go b/gno.land/cmd/gnoland/genesis_validator_remove.go
deleted file mode 100644
index 48a15a9abaf..00000000000
--- a/gno.land/cmd/gnoland/genesis_validator_remove.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package main
-
-import (
- "context"
- "errors"
- "fmt"
-
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/crypto"
-)
-
-var errValidatorNotPresent = errors.New("validator not present in genesis.json")
-
-// newValidatorRemoveCmd creates the genesis validator remove subcommand
-func newValidatorRemoveCmd(rootCfg *validatorCfg, io commands.IO) *commands.Command {
- return commands.NewCommand(
- commands.Metadata{
- Name: "remove",
- ShortUsage: "validator remove [flags]",
- ShortHelp: "removes a validator from the genesis.json",
- },
- commands.NewEmptyConfig(),
- func(_ context.Context, _ []string) error {
- return execValidatorRemove(rootCfg, io)
- },
- )
-}
-
-func execValidatorRemove(cfg *validatorCfg, io commands.IO) error {
- // Load the genesis
- genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath)
- if loadErr != nil {
- return fmt.Errorf("unable to load genesis, %w", loadErr)
- }
-
- // Check the validator address
- address, err := crypto.AddressFromString(cfg.address)
- if err != nil {
- return fmt.Errorf("invalid validator address, %w", err)
- }
-
- index := -1
-
- for indx, validator := range genesis.Validators {
- if validator.Address == address {
- index = indx
-
- break
- }
- }
-
- if index < 0 {
- return errors.New("validator not present in genesis.json")
- }
-
- // Drop the validator
- genesis.Validators = append(genesis.Validators[:index], genesis.Validators[index+1:]...)
-
- // Save the updated genesis
- if err := genesis.SaveAs(cfg.genesisPath); err != nil {
- return fmt.Errorf("unable to save genesis.json, %w", err)
- }
-
- io.Printfln(
- "Validator with address %s removed from genesis file",
- cfg.address,
- )
-
- return nil
-}
diff --git a/gno.land/cmd/gnoland/genesis_validator_remove_test.go b/gno.land/cmd/gnoland/genesis_validator_remove_test.go
deleted file mode 100644
index e73e867c5c3..00000000000
--- a/gno.land/cmd/gnoland/genesis_validator_remove_test.go
+++ /dev/null
@@ -1,133 +0,0 @@
-package main
-
-import (
- "context"
- "testing"
-
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/testutils"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestGenesis_Validator_Remove(t *testing.T) {
- t.Parallel()
-
- t.Run("invalid genesis file", func(t *testing.T) {
- t.Parallel()
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "validator",
- "remove",
- "--genesis-path",
- "dummy-path",
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error())
- })
-
- t.Run("invalid validator address", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- genesis := getDefaultGenesis()
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "validator",
- "remove",
- "--genesis-path",
- tempGenesis.Name(),
- "--address",
- "dummyaddress",
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, "invalid validator address")
- })
-
- t.Run("validator not found", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- dummyKeys := getDummyKeys(t, 2)
- genesis := getDefaultGenesis()
-
- // Set an existing validator
- genesis.Validators = append(genesis.Validators, types.GenesisValidator{
- Address: dummyKeys[0].Address(),
- PubKey: dummyKeys[0],
- Power: 1,
- Name: "example",
- })
-
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "validator",
- "remove",
- "--genesis-path",
- tempGenesis.Name(),
- "--address",
- dummyKeys[1].Address().String(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.ErrorContains(t, cmdErr, errValidatorNotPresent.Error())
- })
-
- t.Run("validator removed", func(t *testing.T) {
- t.Parallel()
-
- tempGenesis, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- dummyKey := getDummyKey(t)
-
- genesis := getDefaultGenesis()
-
- // Set an existing validator
- genesis.Validators = append(genesis.Validators, types.GenesisValidator{
- Address: dummyKey.Address(),
- PubKey: dummyKey,
- Power: 1,
- Name: "example",
- })
-
- require.NoError(t, genesis.SaveAs(tempGenesis.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "validator",
- "remove",
- "--genesis-path",
- tempGenesis.Name(),
- "--address",
- dummyKey.Address().String(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- assert.NoError(t, cmdErr)
- })
-}
diff --git a/gno.land/cmd/gnoland/genesis_verify.go b/gno.land/cmd/gnoland/genesis_verify.go
deleted file mode 100644
index 112b075a58c..00000000000
--- a/gno.land/cmd/gnoland/genesis_verify.go
+++ /dev/null
@@ -1,79 +0,0 @@
-package main
-
-import (
- "context"
- "errors"
- "flag"
- "fmt"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
-)
-
-var errInvalidGenesisState = errors.New("invalid genesis state type")
-
-type verifyCfg struct {
- commonCfg
-}
-
-// newVerifyCmd creates the genesis verify subcommand
-func newVerifyCmd(io commands.IO) *commands.Command {
- cfg := &verifyCfg{}
-
- return commands.NewCommand(
- commands.Metadata{
- Name: "verify",
- ShortUsage: "verify [flags]",
- ShortHelp: "verifies a genesis.json",
- LongHelp: "Verifies a node's genesis.json",
- },
- cfg,
- func(_ context.Context, _ []string) error {
- return execVerify(cfg, io)
- },
- )
-}
-
-func (c *verifyCfg) RegisterFlags(fs *flag.FlagSet) {
- c.commonCfg.RegisterFlags(fs)
-}
-
-func execVerify(cfg *verifyCfg, io commands.IO) error {
- // Load the genesis
- genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath)
- if loadErr != nil {
- return fmt.Errorf("unable to load genesis, %w", loadErr)
- }
-
- // Verify it
- if validateErr := genesis.Validate(); validateErr != nil {
- return fmt.Errorf("unable to verify genesis, %w", validateErr)
- }
-
- // Validate the genesis state
- if genesis.AppState != nil {
- state, ok := genesis.AppState.(gnoland.GnoGenesisState)
- if !ok {
- return errInvalidGenesisState
- }
-
- // Validate the initial transactions
- for _, tx := range state.Txs {
- if validateErr := tx.ValidateBasic(); validateErr != nil {
- return fmt.Errorf("invalid transacton, %w", validateErr)
- }
- }
-
- // Validate the initial balances
- for _, balance := range state.Balances {
- if err := balance.Verify(); err != nil {
- return fmt.Errorf("invalid balance: %w", err)
- }
- }
- }
-
- io.Printfln("Genesis at %s is valid", cfg.genesisPath)
-
- return nil
-}
diff --git a/gno.land/cmd/gnoland/genesis_verify_test.go b/gno.land/cmd/gnoland/genesis_verify_test.go
deleted file mode 100644
index 9c93519e495..00000000000
--- a/gno.land/cmd/gnoland/genesis_verify_test.go
+++ /dev/null
@@ -1,174 +0,0 @@
-package main
-
-import (
- "context"
- "testing"
- "time"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/crypto/mock"
- "github.com/gnolang/gno/tm2/pkg/std"
- "github.com/gnolang/gno/tm2/pkg/testutils"
- "github.com/stretchr/testify/require"
-)
-
-func TestGenesis_Verify(t *testing.T) {
- t.Parallel()
-
- getValidTestGenesis := func() *types.GenesisDoc {
- key := mock.GenPrivKey().PubKey()
-
- return &types.GenesisDoc{
- GenesisTime: time.Now(),
- ChainID: "valid-chain-id",
- ConsensusParams: types.DefaultConsensusParams(),
- Validators: []types.GenesisValidator{
- {
- Address: key.Address(),
- PubKey: key,
- Power: 1,
- Name: "valid validator",
- },
- },
- }
- }
-
- t.Run("invalid txs", func(t *testing.T) {
- t.Parallel()
-
- tempFile, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- g := getValidTestGenesis()
-
- g.AppState = gnoland.GnoGenesisState{
- Balances: []gnoland.Balance{},
- Txs: []std.Tx{
- {},
- },
- }
-
- require.NoError(t, g.SaveAs(tempFile.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "verify",
- "--genesis-path",
- tempFile.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.Error(t, cmdErr)
- })
-
- t.Run("invalid balances", func(t *testing.T) {
- t.Parallel()
-
- tempFile, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- g := getValidTestGenesis()
-
- g.AppState = gnoland.GnoGenesisState{
- Balances: []gnoland.Balance{
- {},
- },
- Txs: []std.Tx{},
- }
-
- require.NoError(t, g.SaveAs(tempFile.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "verify",
- "--genesis-path",
- tempFile.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.Error(t, cmdErr)
- })
-
- t.Run("valid genesis", func(t *testing.T) {
- t.Parallel()
-
- tempFile, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- g := getValidTestGenesis()
- g.AppState = gnoland.GnoGenesisState{
- Balances: []gnoland.Balance{},
- Txs: []std.Tx{},
- }
-
- require.NoError(t, g.SaveAs(tempFile.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "verify",
- "--genesis-path",
- tempFile.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
- })
-
- t.Run("valid genesis, no state", func(t *testing.T) {
- t.Parallel()
-
- tempFile, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- g := getValidTestGenesis()
- require.NoError(t, g.SaveAs(tempFile.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "verify",
- "--genesis-path",
- tempFile.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.NoError(t, cmdErr)
- })
-
- t.Run("invalid genesis state", func(t *testing.T) {
- t.Parallel()
-
- tempFile, cleanup := testutils.NewTestFile(t)
- t.Cleanup(cleanup)
-
- g := getValidTestGenesis()
- g.AppState = "Totally invalid state"
- require.NoError(t, g.SaveAs(tempFile.Name()))
-
- // Create the command
- cmd := newRootCmd(commands.NewTestIO())
- args := []string{
- "genesis",
- "verify",
- "--genesis-path",
- tempFile.Name(),
- }
-
- // Run the command
- cmdErr := cmd.ParseAndRun(context.Background(), args)
- require.Error(t, cmdErr)
- })
-}
diff --git a/gno.land/cmd/gnoland/integration_test.go b/gno.land/cmd/gnoland/integration_test.go
deleted file mode 100644
index 37451df9704..00000000000
--- a/gno.land/cmd/gnoland/integration_test.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package main
-
-import (
- "testing"
-
- "github.com/gnolang/gno/gno.land/pkg/integration"
-)
-
-func TestTestdata(t *testing.T) {
- integration.RunGnolandTestscripts(t, "testdata")
-}
diff --git a/gno.land/cmd/gnoland/root.go b/gno.land/cmd/gnoland/root.go
index 8df716b1fed..c6143ab9cd3 100644
--- a/gno.land/cmd/gnoland/root.go
+++ b/gno.land/cmd/gnoland/root.go
@@ -5,12 +5,8 @@ import (
"os"
"github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/peterbourgon/ff/v3"
- "github.com/peterbourgon/ff/v3/fftoml"
)
-const flagConfigFlag = "flag-config-path"
-
func main() {
cmd := newRootCmd(commands.NewDefaultIO())
@@ -21,11 +17,7 @@ func newRootCmd(io commands.IO) *commands.Command {
cmd := commands.NewCommand(
commands.Metadata{
ShortUsage: " [flags] [...]",
- ShortHelp: "starts the gnoland blockchain node",
- Options: []ff.Option{
- ff.WithConfigFileFlag(flagConfigFlag),
- ff.WithConfigFileParser(fftoml.Parser),
- },
+ ShortHelp: "manages the gnoland blockchain node",
},
commands.NewEmptyConfig(),
commands.HelpExec,
@@ -33,7 +25,6 @@ func newRootCmd(io commands.IO) *commands.Command {
cmd.AddSubCommands(
newStartCmd(io),
- newGenesisCmd(io),
newSecretsCmd(io),
newConfigCmd(io),
)
diff --git a/gno.land/cmd/gnoland/secrets_common.go b/gno.land/cmd/gnoland/secrets_common.go
index d40e90f6b48..500336e3489 100644
--- a/gno.land/cmd/gnoland/secrets_common.go
+++ b/gno.land/cmd/gnoland/secrets_common.go
@@ -8,7 +8,7 @@ import (
"github.com/gnolang/gno/tm2/pkg/amino"
"github.com/gnolang/gno/tm2/pkg/bft/privval"
"github.com/gnolang/gno/tm2/pkg/crypto"
- "github.com/gnolang/gno/tm2/pkg/p2p"
+ "github.com/gnolang/gno/tm2/pkg/p2p/types"
)
var (
@@ -54,7 +54,7 @@ func isValidDirectory(dirPath string) bool {
}
type secretData interface {
- privval.FilePVKey | privval.FilePVLastSignState | p2p.NodeKey
+ privval.FilePVKey | privval.FilePVLastSignState | types.NodeKey
}
// readSecretData reads the secret data from the given path
@@ -145,7 +145,7 @@ func validateValidatorStateSignature(
}
// validateNodeKey validates the node's p2p key
-func validateNodeKey(key *p2p.NodeKey) error {
+func validateNodeKey(key *types.NodeKey) error {
if key.PrivKey == nil {
return errInvalidNodeKey
}
diff --git a/gno.land/cmd/gnoland/secrets_common_test.go b/gno.land/cmd/gnoland/secrets_common_test.go
index 34592c3bd8f..38c4772c705 100644
--- a/gno.land/cmd/gnoland/secrets_common_test.go
+++ b/gno.land/cmd/gnoland/secrets_common_test.go
@@ -5,7 +5,7 @@ import (
"testing"
"github.com/gnolang/gno/tm2/pkg/crypto"
- "github.com/gnolang/gno/tm2/pkg/p2p"
+ "github.com/gnolang/gno/tm2/pkg/p2p/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -26,7 +26,7 @@ func TestCommon_SaveReadData(t *testing.T) {
t.Run("invalid data read path", func(t *testing.T) {
t.Parallel()
- readData, err := readSecretData[p2p.NodeKey]("")
+ readData, err := readSecretData[types.NodeKey]("")
assert.Nil(t, readData)
assert.ErrorContains(
@@ -44,7 +44,7 @@ func TestCommon_SaveReadData(t *testing.T) {
require.NoError(t, saveSecretData("totally valid key", path))
- readData, err := readSecretData[p2p.NodeKey](path)
+ readData, err := readSecretData[types.NodeKey](path)
require.Nil(t, readData)
assert.ErrorContains(t, err, "unable to unmarshal data")
@@ -59,7 +59,7 @@ func TestCommon_SaveReadData(t *testing.T) {
require.NoError(t, saveSecretData(key, path))
- readKey, err := readSecretData[p2p.NodeKey](path)
+ readKey, err := readSecretData[types.NodeKey](path)
require.NoError(t, err)
assert.Equal(t, key, readKey)
diff --git a/gno.land/cmd/gnoland/secrets_get.go b/gno.land/cmd/gnoland/secrets_get.go
index 8d111516816..0a0a714f6ee 100644
--- a/gno.land/cmd/gnoland/secrets_get.go
+++ b/gno.land/cmd/gnoland/secrets_get.go
@@ -12,7 +12,7 @@ import (
"github.com/gnolang/gno/tm2/pkg/bft/privval"
"github.com/gnolang/gno/tm2/pkg/commands"
osm "github.com/gnolang/gno/tm2/pkg/os"
- "github.com/gnolang/gno/tm2/pkg/p2p"
+ "github.com/gnolang/gno/tm2/pkg/p2p/types"
)
var errInvalidSecretsGetArgs = errors.New("invalid number of secrets get arguments provided")
@@ -169,7 +169,7 @@ func readValidatorState(path string) (*validatorStateInfo, error) {
// readNodeID reads the node p2p info from the given path
func readNodeID(path string) (*nodeIDInfo, error) {
- nodeKey, err := readSecretData[p2p.NodeKey](path)
+ nodeKey, err := readSecretData[types.NodeKey](path)
if err != nil {
return nil, fmt.Errorf("unable to read node key, %w", err)
}
@@ -199,7 +199,7 @@ func readNodeID(path string) (*nodeIDInfo, error) {
// constructP2PAddress constructs the P2P address other nodes can use
// to connect directly
-func constructP2PAddress(nodeID p2p.ID, listenAddress string) string {
+func constructP2PAddress(nodeID types.ID, listenAddress string) string {
var (
address string
parts = strings.SplitN(listenAddress, "://", 2)
diff --git a/gno.land/cmd/gnoland/secrets_get_test.go b/gno.land/cmd/gnoland/secrets_get_test.go
index 66e6e3509fc..3dfe0c727dd 100644
--- a/gno.land/cmd/gnoland/secrets_get_test.go
+++ b/gno.land/cmd/gnoland/secrets_get_test.go
@@ -13,7 +13,7 @@ import (
"github.com/gnolang/gno/tm2/pkg/bft/config"
"github.com/gnolang/gno/tm2/pkg/bft/privval"
"github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/p2p"
+ "github.com/gnolang/gno/tm2/pkg/p2p/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -66,7 +66,7 @@ func TestSecrets_Get_All(t *testing.T) {
// Get the node key
nodeKeyPath := filepath.Join(tempDir, defaultNodeKeyName)
- nodeKey, err := readSecretData[p2p.NodeKey](nodeKeyPath)
+ nodeKey, err := readSecretData[types.NodeKey](nodeKeyPath)
require.NoError(t, err)
// Get the validator private key
diff --git a/gno.land/cmd/gnoland/secrets_init.go b/gno.land/cmd/gnoland/secrets_init.go
index 58dd0783f66..9a7ddd106c3 100644
--- a/gno.land/cmd/gnoland/secrets_init.go
+++ b/gno.land/cmd/gnoland/secrets_init.go
@@ -12,7 +12,7 @@ import (
"github.com/gnolang/gno/tm2/pkg/commands"
"github.com/gnolang/gno/tm2/pkg/crypto/ed25519"
osm "github.com/gnolang/gno/tm2/pkg/os"
- "github.com/gnolang/gno/tm2/pkg/p2p"
+ "github.com/gnolang/gno/tm2/pkg/p2p/types"
)
var errOverwriteNotEnabled = errors.New("overwrite not enabled")
@@ -200,10 +200,6 @@ func generateLastSignValidatorState() *privval.FilePVLastSignState {
}
// generateNodeKey generates the p2p node key
-func generateNodeKey() *p2p.NodeKey {
- privKey := ed25519.GenPrivKey()
-
- return &p2p.NodeKey{
- PrivKey: privKey,
- }
+func generateNodeKey() *types.NodeKey {
+ return types.GenerateNodeKey()
}
diff --git a/gno.land/cmd/gnoland/secrets_init_test.go b/gno.land/cmd/gnoland/secrets_init_test.go
index 20e061447f5..7be3650fb4b 100644
--- a/gno.land/cmd/gnoland/secrets_init_test.go
+++ b/gno.land/cmd/gnoland/secrets_init_test.go
@@ -7,7 +7,7 @@ import (
"github.com/gnolang/gno/tm2/pkg/bft/privval"
"github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/p2p"
+ "github.com/gnolang/gno/tm2/pkg/p2p/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -37,7 +37,7 @@ func verifyValidatorState(t *testing.T, path string) {
func verifyNodeKey(t *testing.T, path string) {
t.Helper()
- nodeKey, err := readSecretData[p2p.NodeKey](path)
+ nodeKey, err := readSecretData[types.NodeKey](path)
require.NoError(t, err)
assert.NoError(t, validateNodeKey(nodeKey))
diff --git a/gno.land/cmd/gnoland/secrets_verify.go b/gno.land/cmd/gnoland/secrets_verify.go
index 32e563c1c6f..15fef6649ec 100644
--- a/gno.land/cmd/gnoland/secrets_verify.go
+++ b/gno.land/cmd/gnoland/secrets_verify.go
@@ -8,7 +8,7 @@ import (
"github.com/gnolang/gno/tm2/pkg/bft/privval"
"github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/p2p"
+ "github.com/gnolang/gno/tm2/pkg/p2p/types"
)
type secretsVerifyCfg struct {
@@ -146,7 +146,7 @@ func readAndVerifyValidatorState(path string, io commands.IO) (*privval.FilePVLa
// readAndVerifyNodeKey reads the node p2p key from the given path and verifies it
func readAndVerifyNodeKey(path string, io commands.IO) error {
- nodeKey, err := readSecretData[p2p.NodeKey](path)
+ nodeKey, err := readSecretData[types.NodeKey](path)
if err != nil {
return fmt.Errorf("unable to read node p2p key, %w", err)
}
diff --git a/gno.land/cmd/gnoland/secrets_verify_test.go b/gno.land/cmd/gnoland/secrets_verify_test.go
index 513d7c8b503..67630aaaa4a 100644
--- a/gno.land/cmd/gnoland/secrets_verify_test.go
+++ b/gno.land/cmd/gnoland/secrets_verify_test.go
@@ -8,7 +8,7 @@ import (
"github.com/gnolang/gno/tm2/pkg/bft/privval"
"github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/p2p"
+ "github.com/gnolang/gno/tm2/pkg/p2p/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -347,7 +347,7 @@ func TestSecrets_Verify_Single(t *testing.T) {
dirPath := t.TempDir()
path := filepath.Join(dirPath, defaultNodeKeyName)
- invalidNodeKey := &p2p.NodeKey{
+ invalidNodeKey := &types.NodeKey{
PrivKey: nil, // invalid
}
diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go
index 21f0cb4b1a6..4f380031be4 100644
--- a/gno.land/cmd/gnoland/start.go
+++ b/gno.land/cmd/gnoland/start.go
@@ -14,6 +14,7 @@ import (
"time"
"github.com/gnolang/gno/gno.land/pkg/gnoland"
+ "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
"github.com/gnolang/gno/gno.land/pkg/log"
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
@@ -25,6 +26,8 @@ import (
"github.com/gnolang/gno/tm2/pkg/crypto"
"github.com/gnolang/gno/tm2/pkg/events"
osm "github.com/gnolang/gno/tm2/pkg/os"
+
+ "github.com/gnolang/gno/tm2/pkg/std"
"github.com/gnolang/gno/tm2/pkg/telemetry"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
@@ -42,18 +45,20 @@ var startGraphic = strings.ReplaceAll(`
/___/
`, "'", "`")
+// Keep in sync with contribs/gnogenesis/internal/txs/txs_add_packages.go
+var genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000)))
+
type startCfg struct {
- gnoRootDir string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
- skipFailingGenesisTxs bool // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
- genesisBalancesFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
- genesisTxsFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
- genesisRemote string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
- genesisFile string
- chainID string
- dataDir string
- genesisMaxVMCycles int64
- config string
- lazyInit bool
+ gnoRootDir string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
+ skipFailingGenesisTxs bool // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
+ skipGenesisSigVerification bool // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
+ genesisBalancesFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
+ genesisTxsFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
+ genesisRemote string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
+ genesisFile string
+ chainID string
+ dataDir string
+ lazyInit bool
logLevel string
logFormat string
@@ -79,7 +84,6 @@ func newStartCmd(io commands.IO) *commands.Command {
func (c *startCfg) RegisterFlags(fs *flag.FlagSet) {
gnoroot := gnoenv.RootDir()
defaultGenesisBalancesFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_balances.txt")
- defaultGenesisTxsFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_txs.jsonl")
fs.BoolVar(
&c.skipFailingGenesisTxs,
@@ -88,6 +92,13 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) {
"don't panic when replaying invalid genesis txs",
)
+ fs.BoolVar(
+ &c.skipGenesisSigVerification,
+ "skip-genesis-sig-verification",
+ false,
+ "don't panic when replaying invalidly signed genesis txs",
+ )
+
fs.StringVar(
&c.genesisBalancesFile,
"genesis-balances-file",
@@ -98,7 +109,7 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) {
fs.StringVar(
&c.genesisTxsFile,
"genesis-txs-file",
- defaultGenesisTxsFile,
+ "",
"initial txs to replay",
)
@@ -137,20 +148,6 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) {
"replacement for '%%REMOTE%%' in genesis",
)
- fs.Int64Var(
- &c.genesisMaxVMCycles,
- "genesis-max-vm-cycles",
- 100_000_000,
- "set maximum allowed vm cycles per operation. Zero means no limit.",
- )
-
- fs.StringVar(
- &c.config,
- flagConfigFlag,
- "",
- "the flag config file (optional)",
- )
-
fs.StringVar(
&c.logLevel,
"log-level",
@@ -225,7 +222,7 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error {
)
// Init a new genesis.json
- if err := lazyInitGenesis(io, c, genesisPath, privateKey.GetPubKey()); err != nil {
+ if err := lazyInitGenesis(io, c, genesisPath, privateKey.Key.PrivKey); err != nil {
return fmt.Errorf("unable to initialize genesis.json, %w", err)
}
}
@@ -242,9 +239,19 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error {
// Create a top-level shared event switch
evsw := events.NewEventSwitch()
+ minGasPrices := cfg.Application.MinGasPrices
// Create application and node
- cfg.LocalApp, err = gnoland.NewApp(nodeDir, c.skipFailingGenesisTxs, evsw, logger)
+ cfg.LocalApp, err = gnoland.NewApp(
+ nodeDir,
+ gnoland.GenesisAppConfig{
+ SkipFailingTxs: c.skipFailingGenesisTxs,
+ SkipSigVerification: c.skipGenesisSigVerification,
+ },
+ evsw,
+ logger,
+ minGasPrices,
+ )
if err != nil {
return fmt.Errorf("unable to create the Gnoland app, %w", err)
}
@@ -340,7 +347,7 @@ func lazyInitGenesis(
io commands.IO,
c *startCfg,
genesisPath string,
- publicKey crypto.PubKey,
+ privateKey crypto.PrivKey,
) error {
// Check if the genesis.json is present
if osm.FileExists(genesisPath) {
@@ -348,7 +355,7 @@ func lazyInitGenesis(
}
// Generate the new genesis.json file
- if err := generateGenesisFile(genesisPath, publicKey, c); err != nil {
+ if err := generateGenesisFile(genesisPath, privateKey, c); err != nil {
return fmt.Errorf("unable to generate genesis file, %w", err)
}
@@ -373,24 +380,38 @@ func initializeLogger(io io.WriteCloser, logLevel, logFormat string) (*zap.Logge
return log.GetZapLoggerFn(format)(io, level), nil
}
-func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) error {
+func generateGenesisFile(genesisFile string, privKey crypto.PrivKey, c *startCfg) error {
+ var (
+ pubKey = privKey.PubKey()
+ // There is an active constraint for gno.land transactions:
+ //
+ // All transaction messages' (MsgSend, MsgAddPkg...) "author" field,
+ // specific to the message type ("creator", "sender"...), must match
+ // the signature address contained in the transaction itself.
+ // This means that if MsgSend is originating from address A,
+ // the owner of the private key for address A needs to sign the transaction
+ // containing the message. Every message in a transaction needs to
+ // originate from the same account that signed the transaction
+ txSender = pubKey.Address()
+ )
+
gen := &bft.GenesisDoc{}
gen.GenesisTime = time.Now()
gen.ChainID = c.chainID
gen.ConsensusParams = abci.ConsensusParams{
Block: &abci.BlockParams{
// TODO: update limits.
- MaxTxBytes: 1_000_000, // 1MB,
- MaxDataBytes: 2_000_000, // 2MB,
- MaxGas: 100_000_000, // 100M gas
- TimeIotaMS: 100, // 100ms
+ MaxTxBytes: 1_000_000, // 1MB,
+ MaxDataBytes: 2_000_000, // 2MB,
+ MaxGas: 3_000_000_000, // 3B gas
+ TimeIotaMS: 100, // 100ms
},
}
gen.Validators = []bft.GenesisValidator{
{
- Address: pk.Address(),
- PubKey: pk,
+ Address: pubKey.Address(),
+ PubKey: pubKey,
Power: 10,
Name: "testvalidator",
},
@@ -404,25 +425,46 @@ func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) erro
// Load examples folder
examplesDir := filepath.Join(c.gnoRootDir, "examples")
- pkgsTxs, err := gnoland.LoadPackagesFromDir(examplesDir, genesisDeployAddress, genesisDeployFee)
+ pkgsTxs, err := gnoland.LoadPackagesFromDir(examplesDir, txSender, genesisDeployFee)
if err != nil {
return fmt.Errorf("unable to load examples folder: %w", err)
}
// Load Genesis TXs
- genesisTxs, err := gnoland.LoadGenesisTxsFile(c.genesisTxsFile, c.chainID, c.genesisRemote)
- if err != nil {
- return fmt.Errorf("unable to load genesis txs file: %w", err)
+ var genesisTxs []gnoland.TxWithMetadata
+
+ if c.genesisTxsFile != "" {
+ genesisTxs, err = gnoland.LoadGenesisTxsFile(c.genesisTxsFile, c.chainID, c.genesisRemote)
+ if err != nil {
+ return fmt.Errorf("unable to load genesis txs file: %w", err)
+ }
}
genesisTxs = append(pkgsTxs, genesisTxs...)
- // Construct genesis AppState.
- gen.AppState = gnoland.GnoGenesisState{
- Balances: balances,
- Txs: genesisTxs,
+ // Sign genesis transactions, with the default key (test1)
+ if err = gnoland.SignGenesisTxs(genesisTxs, privKey, c.chainID); err != nil {
+ return fmt.Errorf("unable to sign genesis txs: %w", err)
}
+ // Make sure the genesis transaction author has sufficient
+ // balance to cover transaction deployments in genesis.
+ //
+ // During the init-chainer process, the account that authors the
+ // genesis transactions needs to have a sufficient balance
+ // to cover outstanding transaction costs.
+ // Since the cost can't be estimated upfront at this point, the balance
+ // set is an arbitrary value based on a "best guess" basis.
+ // There should be a larger discussion if genesis transactions should consume gas, at all
+ deployerBalance := int64(len(genesisTxs)) * 10_000_000 // ~10 GNOT per tx
+ balances.Set(txSender, std.NewCoins(std.NewCoin("ugnot", deployerBalance)))
+
+ // Construct genesis AppState.
+ defaultGenState := gnoland.DefaultGenState()
+ defaultGenState.Balances = balances.List()
+ defaultGenState.Txs = genesisTxs
+ gen.AppState = defaultGenState
+
// Write genesis state
if err := gen.SaveAs(genesisFile); err != nil {
return fmt.Errorf("unable to write genesis file %q: %w", genesisFile, err)
diff --git a/gno.land/cmd/gnoland/testdata/addpkg.txtar b/gno.land/cmd/gnoland/testdata/addpkg.txtar
deleted file mode 100644
index 6249d2ff7a0..00000000000
--- a/gno.land/cmd/gnoland/testdata/addpkg.txtar
+++ /dev/null
@@ -1,26 +0,0 @@
-# test for add package
-
-# load hello.gno package located in $WORK directory as gno.land/r/hello
-loadpkg gno.land/r/hello $WORK
-
-## start a new node
-gnoland start
-
-## execute SayHello
-gnokey maketx call -pkgpath gno.land/r/hello -func SayHello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-
-## compare SayHello
-stdout '\("hello world!" string\)'
-stdout OK!
-stdout 'GAS WANTED: 2000000'
-stdout 'GAS USED: \d+'
-stdout 'HEIGHT: \d+'
-stdout 'EVENTS: \[\]'
-stdout 'TX HASH: '
-
--- hello.gno --
-package hello
-
-func SayHello() string {
- return "hello world!"
-}
diff --git a/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar b/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar
deleted file mode 100644
index 5a88fd6d603..00000000000
--- a/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar
+++ /dev/null
@@ -1,88 +0,0 @@
-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/cmd/gnoland/testdata/append.txtar b/gno.land/cmd/gnoland/testdata/append.txtar
deleted file mode 100644
index 46b66f9524b..00000000000
--- a/gno.land/cmd/gnoland/testdata/append.txtar
+++ /dev/null
@@ -1,131 +0,0 @@
-loadpkg gno.land/p/demo/ufmt
-
-# start a new node
-gnoland start
-
-gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/append -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout OK!
-
-# Call Append 1
-gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 2000000 -args '1' -broadcast -chainid=tendermint_test test1
-stdout OK!
-
-gnokey maketx call -pkgpath gno.land/r/append -func AppendNil -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout OK!
-
-# Call Append 2
-gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 2000000 -args '2' -broadcast -chainid=tendermint_test test1
-stdout OK!
-
-# Call Append 3
-gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 2000000 -args '3' -broadcast -chainid=tendermint_test test1
-stdout OK!
-
-# Call render
-gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1
-stdout '("1-2-3-" string)'
-stdout OK!
-
-# Call Pop
-gnokey maketx call -pkgpath gno.land/r/append -func Pop -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout OK!
-
-# Call render
-gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1
-stdout '("2-3-" string)'
-stdout OK!
-
-# Call Append 42
-gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 2000000 -args '42' -broadcast -chainid=tendermint_test test1
-stdout OK!
-
-# Call render
-gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1
-stdout '("2-3-42-" string)'
-stdout OK!
-
-gnokey maketx call -pkgpath gno.land/r/append -func CopyAppend -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout OK!
-
-gnokey maketx call -pkgpath gno.land/r/append -func PopB -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout OK!
-
-# Call render
-gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1
-stdout '("2-3-42-" string)'
-stdout OK!
-
-gnokey maketx call -pkgpath gno.land/r/append -func AppendMoreAndC -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout OK!
-
-gnokey maketx call -pkgpath gno.land/r/append -func ReassignC -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout OK!
-
-gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1
-stdout '("2-3-42-70-100-" string)'
-stdout OK!
-
-gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args 'd' -broadcast -chainid=tendermint_test test1
-stdout '("1-" string)'
-stdout OK!
-
--- append.gno --
-package append
-
-import (
- "gno.land/p/demo/ufmt"
-)
-
-type T struct{ i int }
-
-var a, b, d []T
-var c = []T{{i: 100}}
-
-
-func init() {
- a = make([]T, 0, 1)
-}
-
-func Pop() {
- a = append(a[:0], a[1:]...)
-}
-
-func Append(i int) {
- a = append(a, T{i: i})
-}
-
-func CopyAppend() {
- b = append(a, T{i: 50}, T{i: 60})
-}
-
-func PopB() {
- b = append(b[:0], b[1:]...)
-}
-
-func AppendMoreAndC() {
- // Fill to capacity
- a = append(a, T{i: 70})
- // Above capacity; make new array
- a = append(a, c...)
-}
-
-func ReassignC() {
- c[0] = T{i: 200}
-}
-
-func AppendNil() {
- d = append(d, a...)
-}
-
-func Render(path string) string {
- source := a
- if path == "d" {
- source = d
- }
-
- var s string
- for i:=0;i myrlm.A: PANIC
-! gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-stderr 'invalid non-origin call'
-
-## 2. MsgCall -> myrlm.B: PASS
-gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-stdout 'OK!'
-
-## 3. MsgCall -> myrlm.C: PASS
-gnokey maketx call -pkgpath gno.land/r/myrlm -func C -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-stdout 'OK!'
-
-## 4. MsgCall -> r/foo.A -> myrlm.A: PANIC
-! gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-stderr 'invalid non-origin call'
-
-## 5. MsgCall -> r/foo.B -> myrlm.B: PASS
-gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-stdout 'OK!'
-
-## 6. MsgCall -> r/foo.C -> myrlm.C: PANIC
-! gnokey maketx call -pkgpath gno.land/r/foo -func C -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-stderr 'invalid non-origin call'
-
-## remove due to update to maketx call can only call realm (case 7,8,9)
-## 7. MsgCall -> p/demo/bar.A -> myrlm.A: PANIC
-## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-## stderr 'invalid non-origin call'
-
-## 8. MsgCall -> p/demo/bar.B -> myrlm.B: PASS
-## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-## stdout 'OK!'
-
-## 9. MsgCall -> p/demo/bar.C -> myrlm.C: PANIC
-## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func C -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-## stderr 'invalid non-origin call'
-
-## 10. MsgRun -> run.main -> myrlm.A: PANIC
-! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno
-stderr 'invalid non-origin call'
-
-## 11. MsgRun -> run.main -> myrlm.B: PASS
-gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno
-stdout 'OK!'
-
-## 12. MsgRun -> run.main -> myrlm.C: PANIC
-! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmC.gno
-stderr 'invalid non-origin call'
-
-## 13. MsgRun -> run.main -> foo.A: PANIC
-! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno
-stderr 'invalid non-origin call'
-
-## 14. MsgRun -> run.main -> foo.B: PASS
-gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno
-stdout 'OK!'
-
-## 15. MsgRun -> run.main -> foo.C: PANIC
-! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooC.gno
-stderr 'invalid non-origin call'
-
-## 16. MsgRun -> run.main -> bar.A: PANIC
-! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno
-stderr 'invalid non-origin call'
-
-## 17. MsgRun -> run.main -> bar.B: PASS
-gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno
-stdout 'OK!'
-
-## 18. MsgRun -> run.main -> bar.C: PANIC
-! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/barC.gno
-stderr 'invalid non-origin call'
-
-## remove testcase 19 due to maketx call forced to call a realm
-## 19. MsgCall -> std.AssertOriginCall: pass
-## gnokey maketx call -pkgpath std -func AssertOriginCall -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-## stdout 'OK!'
-
-## 20. MsgRun -> std.AssertOriginCall: PANIC
-! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno
-stderr 'invalid non-origin call'
-
-
--- r/myrlm/rlm.gno --
-package myrlm
-
-import "std"
-
-func A() {
- C()
-}
-
-func B() {
- if false {
- C()
- }
-}
-
-func C() {
- std.AssertOriginCall()
-}
--- r/foo/foo.gno --
-package foo
-
-import "gno.land/r/myrlm"
-
-func A() {
- myrlm.A()
-}
-
-func B() {
- myrlm.B()
-}
-
-func C() {
- myrlm.C()
-}
--- p/demo/bar/bar.gno --
-package bar
-
-import "gno.land/r/myrlm"
-
-func A() {
- myrlm.A()
-}
-
-func B() {
- myrlm.B()
-}
-
-func C() {
- myrlm.C()
-}
--- run/myrlmA.gno --
-package main
-
-import myrlm "gno.land/r/myrlm"
-
-func main() {
- myrlm.A()
-}
--- run/myrlmB.gno --
-package main
-
-import "gno.land/r/myrlm"
-
-func main() {
- myrlm.B()
-}
--- run/myrlmC.gno --
-package main
-
-import "gno.land/r/myrlm"
-
-func main() {
- myrlm.C()
-}
--- run/fooA.gno --
-package main
-
-import "gno.land/r/foo"
-
-func main() {
- foo.A()
-}
--- run/fooB.gno --
-package main
-
-import "gno.land/r/foo"
-
-func main() {
- foo.B()
-}
--- run/fooC.gno --
-package main
-
-import "gno.land/r/foo"
-
-func main() {
- foo.C()
-}
--- run/barA.gno --
-package main
-
-import "gno.land/p/demo/bar"
-
-func main() {
- bar.A()
-}
--- run/barB.gno --
-package main
-
-import "gno.land/p/demo/bar"
-
-func main() {
- bar.B()
-}
--- run/barC.gno --
-package main
-
-import "gno.land/p/demo/bar"
-
-func main() {
- bar.C()
-}
--- run/baz.gno --
-package main
-
-import "std"
-
-func main() {
- std.AssertOriginCall()
-}
diff --git a/gno.land/cmd/gnoland/testdata/event_multi_msg.txtar b/gno.land/cmd/gnoland/testdata/event_multi_msg.txtar
deleted file mode 100644
index 84afe3cc6a4..00000000000
--- a/gno.land/cmd/gnoland/testdata/event_multi_msg.txtar
+++ /dev/null
@@ -1,50 +0,0 @@
-# load the package from $WORK directory
-loadpkg gno.land/r/demo/simple_event $WORK/event
-
-# start a new node
-gnoland start
-
-## test1 account should be available on default
-gnokey query auth/accounts/${USER_ADDR_test1}
-stdout 'height: 0'
-stdout 'data: {'
-stdout ' "BaseAccount": {'
-stdout ' "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",'
-stdout ' "coins": "[0-9]*ugnot",' # dynamic
-stdout ' "public_key": null,'
-stdout ' "account_number": "0",'
-stdout ' "sequence": "0"'
-stdout ' }'
-stdout '}'
-! stderr '.+' # empty
-
-
-## sign
-gnokey sign -tx-path $WORK/multi/multi_msg.tx -chainid=tendermint_test -account-number 0 -account-sequence 0 test1
-stdout 'Tx successfully signed and saved to '
-
-## broadcast
-gnokey broadcast $WORK/multi/multi_msg.tx -quiet=false
-
-stdout OK!
-stdout 'GAS WANTED: 2000000'
-stdout 'GAS USED: [0-9]+'
-stdout 'HEIGHT: [0-9]+'
-stdout 'EVENTS: \[{\"type\":\"TAG\",\"attrs\":\[{\"key\":\"KEY\",\"value\":\"value11\"}\],\"pkg_path\":\"gno.land\/r\/demo\/simple_event\",\"func\":\"Event\"},{\"type\":\"TAG\",\"attrs\":\[{\"key\":\"KEY\",\"value\":\"value22\"}\],\"pkg_path\":\"gno.land\/r\/demo\/simple_event\",\"func\":\"Event\"}\]'
-
-
-
--- event/simple_event.gno --
-package simple_event
-
-import (
- "std"
-)
-
-func Event(value string) {
- std.Emit("TAG", "KEY", value)
-}
-
--- multi/multi_msg.tx --
-{"msg":[{"@type":"/vm.m_call","caller":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","send":"","pkg_path":"gno.land/r/demo/simple_event","func":"Event","args":["value11"]},{"@type":"/vm.m_call","caller":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","send":"","pkg_path":"gno.land/r/demo/simple_event","func":"Event","args":["value22"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":null,"memo":""}
-
diff --git a/gno.land/cmd/gnoland/testdata/ghverify.txtar b/gno.land/cmd/gnoland/testdata/ghverify.txtar
deleted file mode 100644
index f8cd05c762f..00000000000
--- a/gno.land/cmd/gnoland/testdata/ghverify.txtar
+++ /dev/null
@@ -1,39 +0,0 @@
-loadpkg gno.land/r/gnoland/ghverify
-
-# start the node
-gnoland start
-
-# make a verification request
-gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func RequestVerification -args 'deelawn' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout OK!
-
-# request tasks to complete (this is done by the agent)
-gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GnorkleEntrypoint -args 'request' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout '\("\[\{\\"id\\":\\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\\",\\"type\\":\\"0\\",\\"value_type\\":\\"string\\",\\"tasks\\":\[\{\\"gno_address\\":\\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\\",\\"github_handle\\":\\"deelawn\\"\}\]\}\]" string\)'
-
-# a verification request was made but there should be no verified address
-gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetHandleByAddress -args 'g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout ""
-
-# a verification request was made but there should be no verified handle
-gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetAddressByHandle -args 'deelawn' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout ""
-
-# fail on ingestion with a bad task ID
-! gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GnorkleEntrypoint -args 'ingest,a' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stderr 'invalid ingest id: a'
-
-# the agent publishes their response to the task and the verification is complete
-gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GnorkleEntrypoint -args 'ingest,g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5,OK' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout OK!
-
-# get verified github handle by gno address
-gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetHandleByAddress -args 'g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout "deelawn"
-
-# get verified gno address by github handle
-gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetAddressByHandle -args 'deelawn' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"
-
-gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func Render -args '' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout '\("\{\\"deelawn\\": \\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\\"\}" string\)'
\ No newline at end of file
diff --git a/gno.land/cmd/gnoland/testdata/gnoweb_airgapped.txtar b/gno.land/cmd/gnoland/testdata/gnoweb_airgapped.txtar
deleted file mode 100644
index 3ed35a1b1d3..00000000000
--- a/gno.land/cmd/gnoland/testdata/gnoweb_airgapped.txtar
+++ /dev/null
@@ -1,38 +0,0 @@
-# This test ensures that the "full security with airgap" commands, on gnoweb's
-# help page, work as intended.
-
-# load the package from $WORK directory
-loadpkg gno.land/r/demo/echo
-
-# start the node
-gnoland start
-
-# Query account
-gnokey query auth/accounts/${USER_ADDR_test1}
-stdout 'height: 0'
-stdout 'data: {'
-stdout ' "BaseAccount": {'
-stdout ' "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",'
-stdout ' "coins": "[0-9]*ugnot",' # dynamic
-stdout ' "public_key": null,'
-stdout ' "account_number": "0",'
-stdout ' "sequence": "0"'
-stdout ' }'
-stdout '}'
-! stderr '.+' # empty
-
-# Create transaction
-gnokey maketx call -pkgpath "gno.land/r/demo/echo" -func "Render" -gas-fee 1000000ugnot -gas-wanted 2000000 -send "" -args "HELLO" test1
-cp stdout call.tx
-
-# Sign
-gnokey sign -tx-path $WORK/call.tx -chainid "tendermint_test" -account-number 0 -account-sequence 0 test1
-cmpenv stdout sign.stdout.golden
-
-gnokey broadcast $WORK/call.tx
-stdout '("HELLO" string)'
-stdout 'GAS WANTED: 2000000'
-
--- sign.stdout.golden --
-
-Tx successfully signed and saved to $WORK/call.tx
diff --git a/gno.land/cmd/gnoland/testdata/grc20_invalid_address.txtar b/gno.land/cmd/gnoland/testdata/grc20_invalid_address.txtar
deleted file mode 100644
index da903315333..00000000000
--- a/gno.land/cmd/gnoland/testdata/grc20_invalid_address.txtar
+++ /dev/null
@@ -1,12 +0,0 @@
-# Test for https://github.com/gnolang/gno/pull/1799
-loadpkg gno.land/r/demo/foo20
-
-gnoland start
-
-# execute Faucet
-gnokey maketx call -pkgpath gno.land/r/demo/foo20 -func Faucet -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout 'OK!'
-
-# execute Transfer for invalid address
-! gnokey maketx call -pkgpath gno.land/r/demo/foo20 -func Transfer -args g1ubwj0apf60hd90txhnh855fkac34rxlsvua0aa -args 1 -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stderr '"gnokey" error: --= Error =--\nData: invalid address'
\ No newline at end of file
diff --git a/gno.land/cmd/gnoland/testdata/prevrealm.txtar b/gno.land/cmd/gnoland/testdata/prevrealm.txtar
deleted file mode 100644
index 72a207fae22..00000000000
--- a/gno.land/cmd/gnoland/testdata/prevrealm.txtar
+++ /dev/null
@@ -1,184 +0,0 @@
-# This tests ensure the consistency of the std.PrevRealm function, in the
-# following situations:
-#
-#
-# | Num | Msg Type | Call from | Entry Point | Result |
-# |-----|:--------:|:-------------------:|:---------------:|:------------:|
-# | 1 | MsgCall | wallet direct | myrlm.A() | user address |
-# | 2 | | | myrlm.B() | user address |
-# | 3 | | through /r/foo | myrlm.A() | r/foo |
-# | 4 | | | myrlm.B() | r/foo |
-# | 5 | | through /p/demo/bar | myrlm.A() | user address |
-# | 6 | | | myrlm.B() | user address |
-# | 7 | MsgRun | wallet direct | myrlm.A() | user address |
-# | 8 | | | myrlm.B() | user address |
-# | 9 | | through /r/foo | myrlm.A() | r/foo |
-# | 10 | | | myrlm.B() | r/foo |
-# | 11 | | through /p/demo/bar | myrlm.A() | user address |
-# | 12 | | | myrlm.B() | user address |
-# | 13 | MsgCall | wallet direct | std.PrevRealm() | user address |
-# | 14 | MsgRun | wallet direct | std.PrevRealm() | user address |
-
-# Init
-## deploy myrlm
-loadpkg gno.land/r/myrlm $WORK/r/myrlm
-## deploy r/foo
-loadpkg gno.land/r/foo $WORK/r/foo
-## deploy p/demo/bar
-loadpkg gno.land/p/demo/bar $WORK/p/demo/bar
-
-## start a new node
-gnoland start
-
-env RFOO_ADDR=g1evezrh92xaucffmtgsaa3rvmz5s8kedffsg469
-
-# Test cases
-## 1. MsgCall -> myrlm.A: user address
-gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-stdout ${USER_ADDR_test1}
-
-## 2. MsgCall -> myrealm.B -> myrlm.A: user address
-gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-stdout ${USER_ADDR_test1}
-
-## 3. MsgCall -> r/foo.A -> myrlm.A: r/foo
-gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-stdout ${RFOO_ADDR}
-
-## 4. MsgCall -> r/foo.B -> myrlm.B -> r/foo.A: r/foo
-gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-stdout ${RFOO_ADDR}
-
-## remove due to update to maketx call can only call realm (case 5, 6, 13)
-## 5. MsgCall -> p/demo/bar.A -> myrlm.A: user address
-## gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-## stdout ${USER_ADDR_test1}
-
-## 6. MsgCall -> p/demo/bar.B -> myrlm.B -> r/foo.A: user address
-## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-## stdout ${USER_ADDR_test1}
-
-## 7. MsgRun -> myrlm.A: user address
-gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno
-stdout ${USER_ADDR_test1}
-
-## 8. MsgRun -> myrealm.B -> myrlm.A: user address
-gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno
-stdout ${USER_ADDR_test1}
-
-## 9. MsgRun -> r/foo.A -> myrlm.A: r/foo
-gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno
-stdout ${RFOO_ADDR}
-
-## 10. MsgRun -> r/foo.B -> myrlm.B -> r/foo.A: r/foo
-gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno
-stdout ${RFOO_ADDR}
-
-## 11. MsgRun -> p/demo/bar.A -> myrlm.A: user address
-gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno
-stdout ${USER_ADDR_test1}
-
-## 12. MsgRun -> p/demo/bar.B -> myrlm.B -> r/foo.A: user address
-gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno
-stdout ${USER_ADDR_test1}
-
-## 13. MsgCall -> std.PrevRealm(): user address
-## gnokey maketx call -pkgpath std -func PrevRealm -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1
-## stdout ${USER_ADDR_test1}
-
-## 14. MsgRun -> std.PrevRealm(): user address
-gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno
-stdout ${USER_ADDR_test1}
-
--- r/myrlm/myrlm.gno --
-package myrlm
-
-import "std"
-
-func A() string {
- return std.PrevRealm().Addr().String()
-}
-
-func B() string {
- return A()
-}
--- r/foo/foo.gno --
-package foo
-
-import "gno.land/r/myrlm"
-
-func A() string {
- return myrlm.A()
-}
-
-func B() string {
- return myrlm.B()
-}
--- p/demo/bar/bar.gno --
-package bar
-
-import "gno.land/r/myrlm"
-
-func A() string {
- return myrlm.A()
-}
-
-func B() string {
- return myrlm.B()
-}
--- run/myrlmA.gno --
-package main
-
-import myrlm "gno.land/r/myrlm"
-
-func main() {
- println(myrlm.A())
-}
--- run/myrlmB.gno --
-package main
-
-import "gno.land/r/myrlm"
-
-func main() {
- println(myrlm.B())
-}
--- run/fooA.gno --
-package main
-
-import "gno.land/r/foo"
-
-func main() {
- println(foo.A())
-}
--- run/fooB.gno --
-package main
-
-import "gno.land/r/foo"
-
-func main() {
- println(foo.B())
-}
--- run/barA.gno --
-package main
-
-import "gno.land/p/demo/bar"
-
-func main() {
- println(bar.A())
-}
--- run/barB.gno --
-package main
-
-import "gno.land/p/demo/bar"
-
-func main() {
- println(bar.B())
-}
--- run/baz.gno --
-package main
-
-import "std"
-
-func main() {
- println(std.PrevRealm().Addr().String())
-}
diff --git a/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar b/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar
deleted file mode 100644
index 71ef6400471..00000000000
--- a/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar
+++ /dev/null
@@ -1,88 +0,0 @@
-# test for https://github.com/gnolang/gno/pull/875
-
-## another test user, test2
-adduser test2
-
-## start a new node
-gnoland start
-
-## add realm_banker
-gnokey maketx addpkg -pkgdir $WORK/short -pkgpath gno.land/r/test/realm_banker -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
-
-## add realm_banker with long package_name
-gnokey maketx addpkg -pkgdir $WORK/long -pkgpath gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890 -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
-
-## test2 spend all balance
-gnokey maketx send -send "9999999ugnot" -to g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test2
-
-## check test2 balance
-gnokey query bank/balances/${USER_ADDR_test2}
-stdout ''
-
-## mint coin from banker
-gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Mint -args ${USER_ADDR_test2} -args "ugnot" -args "31337" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
-
-## check balance after minting, without patching banker will return '31337ugnot'
-gnokey query bank/balances/${USER_ADDR_test2}
-stdout '"31337/gno.land/r/test/realm_banker:ugnot"'
-
-## burn coin
-gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Burn -args ${USER_ADDR_test2} -args "ugnot" -args "7" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
-
-## check balance after burning
-gnokey query bank/balances/${USER_ADDR_test2}
-stdout '"31330/gno.land/r/test/realm_banker:ugnot"'
-
-## transfer 1ugnot to test2 for gas-fee of below tx
-gnokey maketx send -send "1ugnot" -to ${USER_ADDR_test2} -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
-
-## transfer coin
-gnokey maketx send -send "1330/gno.land/r/test/realm_banker:ugnot" -to g1yr0dpfgthph7y6mepdx8afuec4q3ga2lg8tjt0 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test2
-
-## check sender balance
-gnokey query bank/balances/${USER_ADDR_test2}
-stdout '"30000/gno.land/r/test/realm_banker:ugnot"'
-
-## check receiver balance
-gnokey query bank/balances/g1yr0dpfgthph7y6mepdx8afuec4q3ga2lg8tjt0
-stdout '"1330/gno.land/r/test/realm_banker:ugnot"'
-
-## mint coin from long named package with banker
-gnokey maketx call -pkgpath gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890 -func Mint -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "ugnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
-gnokey query bank/balances/g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7
-stdout '"100/gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890:ugnot"'
-
--- short/realm_banker.gno --
-package realm_banker
-
-import (
- "std"
-)
-
-func Mint(addr std.Address, denom string, amount int64) {
- banker := std.GetBanker(std.BankerTypeRealmIssue)
- banker.IssueCoin(addr, denom, amount)
-}
-
-func Burn(addr std.Address, denom string, amount int64) {
- banker := std.GetBanker(std.BankerTypeRealmIssue)
- banker.RemoveCoin(addr, denom, amount)
-}
-
--- long/realm_banker.gno --
-// package name is 130 characters long
-package package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890
-
-import (
- "std"
-)
-
-func Mint(addr std.Address, denom string, amount int64) {
- banker := std.GetBanker(std.BankerTypeRealmIssue)
- banker.IssueCoin(addr, denom, amount)
-}
-
-func Burn(addr std.Address, denom string, amount int64) {
- banker := std.GetBanker(std.BankerTypeRealmIssue)
- banker.RemoveCoin(addr, denom, amount)
-}
\ No newline at end of file
diff --git a/gno.land/cmd/gnoland/testdata/wugnot.txtar b/gno.land/cmd/gnoland/testdata/wugnot.txtar
deleted file mode 100644
index 1640909fdb9..00000000000
--- a/gno.land/cmd/gnoland/testdata/wugnot.txtar
+++ /dev/null
@@ -1,45 +0,0 @@
-loadpkg gno.land/r/demo/wugnot
-
-gnoland start
-
-gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1
-stdout '# wrapped GNOT \(\$wugnot\)'
-stdout 'Decimals..: 0'
-stdout 'Total supply..: 0'
-stdout 'Known accounts..: 0'
-stdout 'OK!'
-
-gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Deposit -send 12345678ugnot -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout 'OK!'
-
-gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1
-stdout 'Total supply..: 12345678'
-stdout 'Known accounts..: 1'
-stdout 'OK!'
-
-# XXX: use test2 instead (depends on https://github.com/gnolang/gno/issues/1269#issuecomment-1806386069)
-gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Deposit -send 12345678ugnot -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout 'OK!'
-
-gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1
-stdout 'Total supply..: 24691356'
-stdout 'Known accounts..: 1' # should be 2 once we can use test2
-stdout 'OK!'
-
-# XXX: replace hardcoded address with test3
-gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Transfer -gas-fee 1000000ugnot -gas-wanted 2000000 -args 'g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq' -args '10000000' -broadcast -chainid=tendermint_test test1
-stdout 'OK!'
-
-gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1
-stdout 'Total supply..: 24691356'
-stdout 'Known accounts..: 2' # should be 3 once we can use test2
-stdout 'OK!'
-
-# XXX: use test3 instead (depends on https://github.com/gnolang/gno/issues/1269#issuecomment-1806386069)
-gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Withdraw -args 10000000 -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-stdout 'OK!'
-
-gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1
-stdout 'Total supply..: 14691356'
-stdout 'Known accounts..: 2' # should be 3 once we can use test2
-stdout 'OK!'
diff --git a/gno.land/cmd/gnoland/types.go b/gno.land/cmd/gnoland/types.go
deleted file mode 100644
index a48bfaf7b31..00000000000
--- a/gno.land/cmd/gnoland/types.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package main
-
-import (
- "github.com/gnolang/gno/tm2/pkg/std"
-)
-
-// txStore is a wrapper for TM2 transactions
-type txStore []std.Tx
-
-// leftMerge merges the two tx stores, with
-// preference to the left
-func (i *txStore) leftMerge(b txStore) error {
- // Build out the tx hash map
- txHashMap := make(map[string]struct{}, len(*i))
-
- for _, tx := range *i {
- txHash, err := getTxHash(tx)
- if err != nil {
- return err
- }
-
- txHashMap[txHash] = struct{}{}
- }
-
- for _, tx := range b {
- txHash, err := getTxHash(tx)
- if err != nil {
- return err
- }
-
- if _, exists := txHashMap[txHash]; !exists {
- *i = append(*i, tx)
- }
- }
-
- return nil
-}
diff --git a/gno.land/cmd/gnoweb/CONTRIBUTING.md b/gno.land/cmd/gnoweb/CONTRIBUTING.md
deleted file mode 100644
index 7d7663e8bf7..00000000000
--- a/gno.land/cmd/gnoweb/CONTRIBUTING.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# gno.land Website
-
-The gno.land website has 3 main dependencies:
-
-1. [UmbrellaJs](https://umbrellajs.com/) for DOM operations
-2. [MarkedJs](https://marked.js.org/) for Markdown to html compilation
-3. [HighlightJs](https://highlightjs.org/) for golang syntax highlighting
-4. [DOMPurify](https://github.com/cure53/DOMPurify) to sanitize html (and avoid xss)
-
-Some security considerations:
-| | Umbrella Js | Marked Js | HighlightJs | DOMPurify |
-|---|---|---|---|---|
-| dependencies | 0 | 0 | 0 | 0 |
-| sanitize content | | [no](https://marked.js.org/#usage) | [throws an error](https://github.com/highlightjs/highlight.js/blob/7addd66c19036eccd7c602af61f1ed84d215c77d/src/highlight.js#L741) | [yes](https://github.com/cure53/DOMPurify#readme) |
-
-Best Practices:
-
-- **When using MarkedJs**: Always run the output of the marked compiler inside `DOMPurify.sanitize` before inserting it in the dom with `.innerHtml = `.
-- **When using DOMPurify**: Preferably use `{ USE_PROFILES: { html: true } }` option to allow html only. Content passed in the sanitizer must not be modified afterwards, and must directly be inserted in the DOM with innerHtml. Do not call `DOMPurify.sanitize` with the output of a previous `DOMPurify.sanitize` to avoid any mutation XSS risks.
-- **When using HighlightJs**: always configure it before with `hljs.configure({throwUnescapedHTML: true})` to throw before inserting html in the page if any unexpected html children are detected. The check is done [here](https://github.com/highlightjs/highlight.js/blob/7addd66c19036eccd7c602af61f1ed84d215c77d/src/highlight.js#L741).
diff --git a/gno.land/cmd/gnoweb/README.md b/gno.land/cmd/gnoweb/README.md
index 941d5e4f67e..ccd538c8f70 100644
--- a/gno.land/cmd/gnoweb/README.md
+++ b/gno.land/cmd/gnoweb/README.md
@@ -2,12 +2,4 @@
The gno.land web interface.
-Live demo: https://test3.gno.land/
-
-## Install `gnoweb`
-
-Install and run a local [`gnoland`](../gnoland) instance first.
-
- $> git clone git@github.com:gnolang/gno.git
- $> cd ./gno/gno.land
- $> make install.gnoweb
+Live demo: [https://gno.land/](https://gno.land/) or using `gnodev` from the directory [gnodev](../../../contribs/gnodev).
diff --git a/gno.land/cmd/gnoweb/main.go b/gno.land/cmd/gnoweb/main.go
index 547134548ff..8c0df00aa35 100644
--- a/gno.land/cmd/gnoweb/main.go
+++ b/gno.land/cmd/gnoweb/main.go
@@ -1,60 +1,196 @@
package main
import (
+ "context"
"flag"
"fmt"
+ "net"
"net/http"
"os"
"time"
- // for static files
"github.com/gnolang/gno/gno.land/pkg/gnoweb"
"github.com/gnolang/gno/gno.land/pkg/log"
+ "github.com/gnolang/gno/tm2/pkg/commands"
+ "go.uber.org/zap"
"go.uber.org/zap/zapcore"
- // for error types
- // "github.com/gnolang/gno/tm2/pkg/sdk" // for baseapp (info, status)
)
+type webCfg struct {
+ chainid string
+ remote string
+ remoteHelp string
+ bind string
+ faucetURL string
+ assetsDir string
+ analytics bool
+ json bool
+ html bool
+ verbose bool
+}
+
+var defaultWebOptions = webCfg{
+ chainid: "dev",
+ remote: "127.0.0.1:26657",
+ bind: ":8888",
+}
+
func main() {
- err := runMain(os.Args[1:])
- if err != nil {
- _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err)
- os.Exit(1)
- }
+ var cfg webCfg
+
+ stdio := commands.NewDefaultIO()
+ cmd := commands.NewCommand(
+ commands.Metadata{
+ Name: "gnoweb",
+ ShortUsage: "gnoweb [flags] [path ...]",
+ ShortHelp: "runs gno.land web interface",
+ LongHelp: `gnoweb web interface`,
+ },
+ &cfg,
+ func(ctx context.Context, args []string) error {
+ run, err := setupWeb(&cfg, args, stdio)
+ if err != nil {
+ return err
+ }
+
+ return run()
+ })
+
+ cmd.Execute(context.Background(), os.Args[1:])
}
-func runMain(args []string) error {
- var (
- fs = flag.NewFlagSet("gnoweb", flag.ContinueOnError)
- cfg = gnoweb.NewDefaultConfig()
- bindAddress string
- )
- fs.StringVar(&cfg.RemoteAddr, "remote", cfg.RemoteAddr, "remote gnoland node address")
- fs.StringVar(&cfg.CaptchaSite, "captcha-site", cfg.CaptchaSite, "recaptcha site key (if empty, captcha are disabled)")
- fs.StringVar(&cfg.FaucetURL, "faucet-url", cfg.FaucetURL, "faucet server URL")
- fs.StringVar(&cfg.ViewsDir, "views-dir", cfg.ViewsDir, "views directory location") // XXX: replace with goembed
- fs.StringVar(&cfg.HelpChainID, "help-chainid", cfg.HelpChainID, "help page's chainid")
- fs.StringVar(&cfg.HelpRemote, "help-remote", cfg.HelpRemote, "help page's remote addr")
- fs.BoolVar(&cfg.WithAnalytics, "with-analytics", cfg.WithAnalytics, "enable privacy-first analytics")
- fs.StringVar(&bindAddress, "bind", "127.0.0.1:8888", "server listening address")
-
- if err := fs.Parse(args); err != nil {
- return err
+func (c *webCfg) RegisterFlags(fs *flag.FlagSet) {
+ fs.StringVar(
+ &c.remote,
+ "remote",
+ defaultWebOptions.remote,
+ "remote gno.land node address",
+ )
+
+ fs.StringVar(
+ &c.remoteHelp,
+ "help-remote",
+ defaultWebOptions.remoteHelp,
+ "help page's remote address",
+ )
+
+ fs.StringVar(
+ &c.assetsDir,
+ "assets-dir",
+ defaultWebOptions.assetsDir,
+ "if not empty, will be use as assets directory",
+ )
+
+ fs.StringVar(
+ &c.chainid,
+ "help-chainid",
+ defaultWebOptions.chainid,
+ "Deprecated: use `chainid` instead",
+ )
+
+ fs.StringVar(
+ &c.chainid,
+ "chainid",
+ defaultWebOptions.chainid,
+ "target chain id",
+ )
+
+ fs.StringVar(
+ &c.bind,
+ "bind",
+ defaultWebOptions.bind,
+ "gnoweb listener",
+ )
+
+ fs.StringVar(
+ &c.faucetURL,
+ "faucet-url",
+ defaultWebOptions.faucetURL,
+ "The faucet URL will redirect the user when they access `/faucet`.",
+ )
+
+ fs.BoolVar(
+ &c.json,
+ "json",
+ defaultWebOptions.json,
+ "display log in json format",
+ )
+
+ fs.BoolVar(
+ &c.html,
+ "html",
+ defaultWebOptions.html,
+ "enable unsafe html",
+ )
+
+ fs.BoolVar(
+ &c.analytics,
+ "with-analytics",
+ defaultWebOptions.analytics,
+ "nable privacy-first analytics",
+ )
+
+ fs.BoolVar(
+ &c.verbose,
+ "v",
+ defaultWebOptions.verbose,
+ "verbose logging mode",
+ )
+}
+
+func setupWeb(cfg *webCfg, _ []string, io commands.IO) (func() error, error) {
+ // Setup logger
+ level := zapcore.InfoLevel
+ if cfg.verbose {
+ level = zapcore.DebugLevel
+ }
+ var zapLogger *zap.Logger
+ if cfg.json {
+ zapLogger = log.NewZapJSONLogger(io.Out(), level)
+ } else {
+ zapLogger = log.NewZapConsoleLogger(io.Out(), level)
}
+ defer zapLogger.Sync()
- zapLogger := log.NewZapConsoleLogger(os.Stdout, zapcore.DebugLevel)
logger := log.ZapLoggerToSlog(zapLogger)
- logger.Info("Running", "listener", "http://"+bindAddress)
- server := &http.Server{
- Addr: bindAddress,
- ReadHeaderTimeout: 60 * time.Second,
- Handler: gnoweb.MakeApp(logger, cfg).Router,
+ // Setup app
+ appcfg := gnoweb.NewDefaultAppConfig()
+ appcfg.ChainID = cfg.chainid
+ appcfg.NodeRemote = cfg.remote
+ appcfg.RemoteHelp = cfg.remoteHelp
+ if appcfg.RemoteHelp == "" {
+ appcfg.RemoteHelp = appcfg.NodeRemote
+ }
+ appcfg.Analytics = cfg.analytics
+ appcfg.UnsafeHTML = cfg.html
+ appcfg.FaucetURL = cfg.faucetURL
+ appcfg.AssetsDir = cfg.assetsDir
+ app, err := gnoweb.NewRouter(logger, appcfg)
+ if err != nil {
+ return nil, fmt.Errorf("unable to start gnoweb app: %w", err)
+ }
+
+ // Resolve binding address
+ bindaddr, err := net.ResolveTCPAddr("tcp", cfg.bind)
+ if err != nil {
+ return nil, fmt.Errorf("unable to resolve listener %q: %w", cfg.bind, err)
}
- if err := server.ListenAndServe(); err != nil {
- logger.Error("HTTP server stopped", " error:", err)
+ logger.Info("Running", "listener", bindaddr.String())
+
+ // Setup server
+ server := &http.Server{
+ Handler: app,
+ Addr: bindaddr.String(),
+ ReadHeaderTimeout: 60 * time.Second,
}
- return zapLogger.Sync()
+ return func() error {
+ if err := server.ListenAndServe(); err != nil {
+ logger.Error("HTTP server stopped", "error", err)
+ return commands.ExitCodeError(1)
+ }
+ return nil
+ }, nil
}
diff --git a/gno.land/cmd/gnoweb/main_test.go b/gno.land/cmd/gnoweb/main_test.go
index 640c4763140..37006c18c93 100644
--- a/gno.land/cmd/gnoweb/main_test.go
+++ b/gno.land/cmd/gnoweb/main_test.go
@@ -1,14 +1,25 @@
package main
import (
- "errors"
- "flag"
+ "os"
"testing"
+
+ "github.com/gnolang/gno/tm2/pkg/commands"
+ "github.com/stretchr/testify/require"
)
-func TestFlagHelp(t *testing.T) {
- err := runMain([]string{"-h"})
- if !errors.Is(err, flag.ErrHelp) {
- t.Errorf("should display usage")
- }
+func TestSetupWeb(t *testing.T) {
+ opts := defaultWebOptions
+ opts.bind = "127.0.0.1:0" // random port
+ stdio := commands.NewDefaultIO()
+
+ // Open /dev/null as a write-only file
+ devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0o644)
+ require.NoError(t, err)
+ defer devNull.Close()
+
+ stdio.SetOut(devNull)
+
+ _, err = setupWeb(&opts, []string{}, stdio)
+ require.NoError(t, err)
}
diff --git a/gno.land/genesis/README.md b/gno.land/genesis/README.md
index 55fdb3d0dfd..4fb81baaaa0 100644
--- a/gno.land/genesis/README.md
+++ b/gno.land/genesis/README.md
@@ -1,3 +1,3 @@
-# Gno.land genesis
+# gno.land genesis
**WIP: see https://github.com/gnolang/independence-day**
diff --git a/gno.land/genesis/genesis_balances.txt b/gno.land/genesis/genesis_balances.txt
index fa3232149c1..c372d7f9fd7 100644
--- a/gno.land/genesis/genesis_balances.txt
+++ b/gno.land/genesis/genesis_balances.txt
@@ -16,7 +16,8 @@ g13d7jc32adhc39erm5me38w5v7ej7lpvlnqjk73=1000000000000ugnot # faucet3 (devx)
g18l9us6trqaljw39j94wzf5ftxmd9qqkvrxghd2=1000000000000ugnot # faucet4 (adena)
# Contributors premine & GitHub requests (closed).
-g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq=10000000000ugnot # @moul
+g1manfred47kzduec920z88wfr64ylksmdcedlf5=10000000000ugnot # @moul
+g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq=10000000000ugnot # @manfred
g14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa=10000000000ugnot # @piux2
g15gdm49ktawvkrl88jadqpucng37yxutucuwaef=10000000000ugnot # @chadwick
g1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s=10000000000ugnot # @mefodica #83
diff --git a/gno.land/genesis/genesis_params.toml b/gno.land/genesis/genesis_params.toml
new file mode 100644
index 00000000000..fb080024624
--- /dev/null
+++ b/gno.land/genesis/genesis_params.toml
@@ -0,0 +1,29 @@
+
+## gno.land
+["gno.land/r/sys/params.sys"]
+ users_pkgpath.string = "gno.land/r/sys/users" # if empty, no namespace support.
+ # TODO: validators_pkgpath.string = "gno.land/r/sys/validators"
+ # TODO: rewards_pkgpath.string = "gno.land/r/sys/rewards"
+ # TODO: token_lock.bool = true
+
+## gnovm
+["gno.land/r/sys/params.vm"]
+ chain_domain.string = "gno.land"
+ # TODO: max_gas.int64 = 100_000_000
+ # TODO: chain_tz.string = "UTC"
+ # TODO: default_storage_allowance.string = ""
+
+## tm2
+["gno.land/r/sys/params.tm2"]
+
+## misc
+["gno.land/r/sys/params.misc"]
+
+## testing
+# do not remove these lines. they are needed for a txtar integration test.
+["gno.land/r/sys/params.test"]
+ foo.string = "bar"
+ foo.int64 = -1337
+ foo.uint64 = 42
+ foo.bool = true
+ #foo.bytes = todo
diff --git a/gno.land/genesis/genesis_txs.jsonl b/gno.land/genesis/genesis_txs.jsonl
index daf9fbdc5d4..9027d51c0ac 100644
--- a/gno.land/genesis/genesis_txs.jsonl
+++ b/gno.land/genesis/genesis_txs.jsonl
@@ -1,17 +1,17 @@
-{"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":""}
-{"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Simple echo example with coins","This is a simple test realm contract that demonstrates how to use the banker.\n\nSee [gno.land/r/demo/banktest/banktest.gno](/r/demo/banktest/banktest.gno) to see the original contract code.\n\nThis article will go through each line to explain how it works.\n\n```go\npackage banktest\n```\n\nThis package is locally named \"banktest\" (could be anything).\n\n```go\nimport (\n\t\"std\"\n)\n```\n\nThe \"std\" package is defined by the gno code in stdlibs/std/. \u003c/br\u003e\nSelf explanatory; and you'll see more usage from std later.\n\n```go\ntype activity struct {\n\tcaller std.Address\n\tsent std.Coins\n\treturned std.Coins\n\ttime std.Time\n}\n\nfunc (act *activity) String() string {\n\treturn act.caller.String() + \" \" +\n\t\tact.sent.String() + \" sent, \" +\n\t\tact.returned.String() + \" returned, at \" +\n\t\tstd.FormatTimestamp(act.time, \"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n```\n\nThis is just maintaining a list of recent activity to this contract.\nNotice that the \"latest\" variable is defined \"globally\" within\nthe context of the realm with path \"gno.land/r/demo/banktest\".\n\nThis means that calls to functions defined within this package\nare encapsulated within this \"data realm\", where the data is \nmutated based on transactions that can potentially cross many\nrealm and non-realm packge boundaries (in the call stack).\n\n```go\n// Deposit will take the coins (to the realm's pkgaddr) or return them to user.\nfunc Deposit(returnDenom string, returnAmount int64) string {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tsend := std.Coins{{returnDenom, returnAmount}}\n```\n\nThis is the beginning of the definition of the contract function named\n\"Deposit\". `std.AssertOriginCall() asserts that this function was called by a\ngno transactional Message. The caller is the user who signed off on this\ntransactional message. Send is the amount of deposit sent along with this\nmessage.\n\n```go\n\t// record activity\n\tact := \u0026activity{\n\t\tcaller: caller,\n\t\tsent: std.GetOrigSend(),\n\t\treturned: send,\n\t\ttime: std.GetTimestamp(),\n\t}\n\tfor i := len(latest) - 2; i \u003e= 0; i-- {\n\t\tlatest[i+1] = latest[i] // shift by +1.\n\t}\n\tlatest[0] = act\n```\n\nUpdating the \"latest\" array for viewing at gno.land/r/demo/banktest: (w/ trailing colon).\n\n```go\n\t// return if any.\n\tif returnAmount \u003e 0 {\n```\n\nIf the user requested the return of coins...\n\n```go\n\t\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n```\n\nuse a std.Banker instance to return any deposited coins to the original sender.\n\n```go\n\t\tpkgaddr := std.GetOrigPkgAddr()\n\t\t// TODO: use std.Coins constructors, this isn't generally safe.\n\t\tbanker.SendCoins(pkgaddr, caller, send)\n\t\treturn \"returned!\"\n```\n\nNotice that each realm package has an associated Cosmos address.\n\n\nFinally, the results are rendered via an ABCI query call when you visit [/r/demo/banktest:](/r/demo/banktest:).\n\n```go\nfunc Render(path string) string {\n\t// get realm coins.\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(std.GetOrigPkgAddr())\n\n\t// render\n\tres := \"\"\n\tres += \"## recent activity\\n\"\n\tres += \"\\n\"\n\tfor _, act := range latest {\n\t\tif act == nil {\n\t\t\tbreak\n\t\t}\n\t\tres += \" * \" + act.String() + \"\\n\"\n\t}\n\tres += \"\\n\"\n\tres += \"## total deposits\\n\"\n\tres += coins.String()\n\treturn res\n}\n```\n\nYou can call this contract yourself, by vistiing [/r/demo/banktest](/r/demo/banktest) and the [quickstart guide](/r/demo/boards:testboard/4).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"iZX/llZlNTdZMLv1goCTgK2bWqzT8enlTq56wMTCpVxJGA0BTvuEM5Nnt9vrnlG6Taqj2GuTrmEnJBkDFTmt9g=="}],"memo":""}
-{"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","TASK: Describe in your words","Describe in an essay (250+ words), on your favorite medium, why you are interested in gno.land and gnolang.\n\nReply here with a URL link to your written piece as a comment, for rewards.\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"4HBNtrta8HdeHj4JTN56PBTRK8GOe31NMRRXDiyYtjozuyRdWfOGEsGjGgHWcoBUJq6DepBgD4FetdqfhZ6TNQ=="}],"memo":""}
-{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Getting Started","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm\nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n\n\n## Build `gnokey`, create your account, and interact with Gno.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### Build `gnokey`.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake\n```\n\n### Generate a seed/mnemonic code.\n\n```bash\n./build/gnokey generate\n```\n\nNOTE: You can generate 24 words with any good bip39 generator.\n\n### Create a new account using your mnemonic.\n\n```bash\n./build/gnokey add KEYNAME --recover\n```\n\nNOTE: `KEYNAME` is your key identifier, and should be changed.\n\n### Verify that you can see your account locally.\n\n```bash\n./build/gnokey list\n```\n\n## Interact with the blockchain:\n\n### Get your current balance, account number, and sequence number.\n\n```bash\n./build/gnokey query auth/accounts/ACCOUNT_ADDR --remote %%REMOTE%%\n```\n\nNOTE: you can retrieve your `ACCOUNT_ADDR` with `./build/gnokey list`.\n\n### Acquire testnet tokens using the official faucet.\n\nGo to https://gno.land/faucet\n\n### Create a board with a smart contract call.\n\nNOTE: `BOARDNAME` will be the slug of the board, and should be changed.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateBoard\" --args \"BOARDNAME\" --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateBoard\n\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards.GetBoardIDFromName(\\\"BOARDNAME\\\")\" --remote %%REMOTE%%\n```\n\n### Create a post of a board with a smart contract call.\n\nNOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateThread\" --args BOARD_ID --args \"Hello gno.land\" --args\\#file \"./examples/gno.land/r/demo/boards/example_post.md\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateThread\n\n### Create a comment to a post.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateReply\" --args \"BOARD_ID\" --args \"1\" --args \"1\" --args \"Nice to meet you too.\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateReply\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:BOARDNAME/1\" --remote %%REMOTE%%\n```\n\n### Render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:gnolang` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:gnolang\"\n```\n\n## Starting a local `gnoland` node:\n\n### Add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mneonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### Start `gnoland` node.\n\n```bash\n./build/gnoland\n```\n\nNOTE: This can be reset with `make reset`\n\n### Publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n\n### Publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 300000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}
-{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post1","First post","Lorem Ipsum","2022-05-20T13:17:22Z","","tag1,tag2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}
-{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post2","Second post","Lorem Ipsum","2022-05-20T13:17:23Z","","tag1,tag3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj:10\ng1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s:1\ng13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8:1\ng1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q:1\ng1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj:1\ng18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0:1\ng19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz:1\ng187982000zsc493znqt828s90cmp6hcp2erhu6m:1\ng1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl:1\ng16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037:1\ng1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5:1\ng1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr:1\ng1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz:1\ng19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w:1\ng1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz:1\ng14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3:1\ng1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0:1\ng15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n:1\ng1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac:1\ng1z629z04f85k4t5gnkk5egpxw9tqxeec435esap:1\ng1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv:1\ng152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv:1\ng1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq:1\ng1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6:1\ng1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q:1\ng1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7:1\ng1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k:1\ng13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll:1\ng19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd:1\ng1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64:1\ng1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw:1\ng19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a:1\ng1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc:1\ng13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6:1\ng1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6:1\ng1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9:1\ng1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea:1\ng1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3:1\ng1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp:1\ng14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5:1\ng19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf:1\ng1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g:1\ng1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r:1\ng1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su:1\ng1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69:1\ng1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6:1\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa:10\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t:5\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"S8iMMzlOMK8dmox78R9Z8+pSsS8YaTCXrIcaHDpiOgkOy7gqoQJ0oftM0zf8zAz4xpezK8Lzg8Q0fCdXJxV76w=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1thlf3yct7n7ex70k0p62user0kn6mj6d3s0cg3\ng1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\ng1manfred47kzduec920z88wfr64ylksmdcedlf5\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"njczE6xYdp01+CaUU/8/v0YC/NuZD06+qLind+ZZEEMNaRe/4Ln+4z7dG6HYlaWUMsyI1KCoB6NIehoE0PZ44Q=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s\ng13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8\ng1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q\ng1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj\ng18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0\ng19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz\ng187982000zsc493znqt828s90cmp6hcp2erhu6m\ng1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl\ng16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037\ng1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5\ng1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr\ng1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz\ng19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w\ng1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz\ng14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3\ng1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0\ng15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n\ng1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac\ng1z629z04f85k4t5gnkk5egpxw9tqxeec435esap\ng1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv\ng152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv\ng1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq\ng1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6\ng1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q\ng1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7\ng1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k\ng13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll\ng19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd\ng1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64\ng1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw\ng19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a\ng1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc\ng13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6\ng1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6\ng1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9\ng1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea\ng1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3\ng1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp\ng14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5\ng19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf\ng1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g\ng1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r\ng1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su\ng1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69\ng1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6\ng1manfred47kzduec920z88wfr64ylksmdcedlf5\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t\n"]}],"fee":{"gas_wanted":"4000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"7AmlhZhsVkxCUl0bbpvpPMnIKihwtG7A5IFR6Tg4xStWLgaUr05XmWRKlO2xjstTtwbVKQT5mFL4h5wyX4SQzw=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","administrator","g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"AqCqe0cS55Ym7/BvPDoCDyPP5q8284gecVQ2PMOlq/4lJpO9Q18SOWKI15dMEBY1pT0AYyhCeTirlsM1I3Y4Cg=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1qpymzwx4l4cy6cerdyajp9ksvjsf20rk5y9rtt","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","zo_oma","Love is the encryption key\u003c3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6yg5/iiktruezVw5vZJwLlGwyrvw8RlqOToTRMWXkE2"},"signature":"GGp+bVL2eEvKecPqgcULSABYOSnSMnJzfIsR8ZIRER1GGX/fOiCReX4WKMrGLVROJVfbLQkDRwvhS4TLHlSoSQ=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["g1manfred47kzduec920z88wfr64ylksmdcedlf5","moul","https://github.com/moul"]}],"fee":{"gas_wanted":"2000000","gas_fee":"200000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"9CWeNbKx+hEL+RdHplAVAFntcrAVx5mK9tMqoywuHVoreH844n3yOxddQrGfBk6T2tMBmNWakERRqWZfS+bYAQ=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","piupiu","@piux2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Ar68lqbU2YC63fbMcYUtJhYO3/66APM/EqF7m0nUjGyz"},"signature":"pTUpP0d/XlfVe3TH1hlaoLhKadzIKG1gtQ/Ueuat72p+659RWRea58Z0mk6GgPE/EeTbhMEY45zufevBdGJVoQ=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5","send":"","pkg_path":"gno.land/r/demo/users","func":"Register","args":["g1manfred47kzduec920z88wfr64ylksmdcedlf5","anarcher","https://twitter.com/anarcher"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AjpLbKdQeH+yB/1OCB148l5GlRRrXma71hdA8EES3H7f"},"signature":"pf5xm8oWIQIOEwSGw4icPmynLXb1P1HxKfjeh8UStU1mlIBPKa7yppeIMPpAflC0o2zjFR7Axe7CimAebm3BHg=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g15gdm49ktawvkrl88jadqpucng37yxutucuwaef","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","ideamour","\u003c3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhClx4AsDuX3DNCPxhDwWnrfd4MIZmxJE4vt47ClVvT2"},"signature":"IQe64af878k6HjLDqIJeg27GXAVF6xS+96cDe2jMlxNV6+8sOcuUctp0GiWVnYfN4tpthC6d4WhBo+VlpHqkbg=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateBoard","args":["testboard"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"vzlSxEFh5jOkaSdv3rsV91v/OJKEF2qSuoCpri1u5tRWq62T7xr3KHRCF5qFnn4aQX/yE8g8f/Y//WPOCUGhJw=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Hello World","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm \nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n## Starting the `gnoland` node node/validator.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### build gnoland.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake \n```\n\n### add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mnemonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### start gnoland validator node.\n\n```bash\n./build/gnoland\n```\n\n(This can be reset with `make reset`).\n\n### start gnoland web server (optional).\n\n```bash\ngo run ./gnoland/website\n```\n\n## Signing and broadcasting transactions.\n\n### publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100ugnot --gas-fee 1ugnot --gas-wanted 2000000 \u003e addpkg.avl.unsigned.txt\n./build/gnokey query \"auth/accounts/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n./build/gnokey sign test1 --txpath addpkg.avl.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 0 \u003e addpkg.avl.signed.txt\n./build/gnokey broadcast addpkg.avl.signed.txt --remote %%REMOTE%%\n```\n\n### publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100ugnot --gas-fee 1ugnot --gas-wanted 300000000 \u003e addpkg.boards.unsigned.txt\n./build/gnokey sign test1 --txpath addpkg.boards.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 1 \u003e addpkg.boards.signed.txt\n./build/gnokey broadcast addpkg.boards.signed.txt --remote %%REMOTE%%\n```\n\n### create a board with a smart contract call.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreateBoard --args \"testboard\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createboard.unsigned.txt\n./build/gnokey sign test1 --txpath createboard.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 2 \u003e createboard.signed.txt\n./build/gnokey broadcast createboard.signed.txt --remote %%REMOTE%%\n```\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards.GetBoardIDFromName(\\\"testboard\\\")\"\n```\n\n### create a post of a board with a smart contract call.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreatePost --args 1 --args \"Hello World\" --args#file \"./examples/gno.land/r/demo/boards/README.md\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createpost.unsigned.txt\n./build/gnokey sign test1 --txpath createpost.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 3 \u003e createpost.signed.txt\n./build/gnokey broadcast createpost.signed.txt --remote %%REMOTE%%\n```\n\n### create a comment to a post.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreateReply --args 1 --args 1 --args \"A comment\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createcomment.unsigned.txt\n./build/gnokey sign test1 --txpath createcomment.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 4 \u003e createcomment.signed.txt\n./build/gnokey broadcast createcomment.signed.txt --remote %%REMOTE%%\n```\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:testboard/1\"\n```\n\n### render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:testboard` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:testboard\"\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"V43B1waFxhzheW9TfmCpjLdrC4dC1yjUGES5y3J6QsNar6hRpNz4G1thzWmWK7xXhg8u1PCIpxLxGczKQYhuPw=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","NFT example","NFT's are all the rage these days, for various reasons.\n\nI read over EIP-721 which appears to be the de-facto NFT standard on Ethereum. Then, made a sample implementation of EIP-721 (let's here called GRC-721). The implementation isn't complete, but it demonstrates the main functionality.\n\n - [EIP-721](https://eips.ethereum.org/EIPS/eip-721)\n - [gno.land/r/demo/nft/nft.gno](https://gno.land/r/demo/nft/nft.gno)\n - [zrealm_nft3.gno test](https://github.com/gnolang/gno/blob/master/examples/gno.land/r/demo/nft/z_3_filetest.gno)\n\nIn short, this demonstrates how to implement Ethereum contract interfaces in gno.land; by using only standard Go language features.\n\nPlease leave a comment ([guide](https://gno.land/r/demo/boards:testboard/1)).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"ZXfrTiHxPFQL8uSm+Tv7WXIHPMca9okhm94RAlC6YgNbB1VHQYYpoP4w+cnL3YskVzGrOZxensXa9CAZ+cNNeg=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Simple echo example with coins","This is a simple test realm contract that demonstrates how to use the banker.\n\nSee [gno.land/r/demo/banktest/banktest.gno](/r/demo/banktest/banktest.gno) to see the original contract code.\n\nThis article will go through each line to explain how it works.\n\n```go\npackage banktest\n```\n\nThis package is locally named \"banktest\" (could be anything).\n\n```go\nimport (\n\t\"std\"\n)\n```\n\nThe \"std\" package is defined by the gno code in stdlibs/std/. \u003c/br\u003e\nSelf explanatory; and you'll see more usage from std later.\n\n```go\ntype activity struct {\n\tcaller std.Address\n\tsent std.Coins\n\treturned std.Coins\n\ttime std.Time\n}\n\nfunc (act *activity) String() string {\n\treturn act.caller.String() + \" \" +\n\t\tact.sent.String() + \" sent, \" +\n\t\tact.returned.String() + \" returned, at \" +\n\t\tstd.FormatTimestamp(act.time, \"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n```\n\nThis is just maintaining a list of recent activity to this contract.\nNotice that the \"latest\" variable is defined \"globally\" within\nthe context of the realm with path \"gno.land/r/demo/banktest\".\n\nThis means that calls to functions defined within this package\nare encapsulated within this \"data realm\", where the data is \nmutated based on transactions that can potentially cross many\nrealm and non-realm packge boundaries (in the call stack).\n\n```go\n// Deposit will take the coins (to the realm's pkgaddr) or return them to user.\nfunc Deposit(returnDenom string, returnAmount int64) string {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tsend := std.Coins{{returnDenom, returnAmount}}\n```\n\nThis is the beginning of the definition of the contract function named\n\"Deposit\". `std.AssertOriginCall() asserts that this function was called by a\ngno transactional Message. The caller is the user who signed off on this\ntransactional message. Send is the amount of deposit sent along with this\nmessage.\n\n```go\n\t// record activity\n\tact := \u0026activity{\n\t\tcaller: caller,\n\t\tsent: std.GetOrigSend(),\n\t\treturned: send,\n\t\ttime: std.GetTimestamp(),\n\t}\n\tfor i := len(latest) - 2; i \u003e= 0; i-- {\n\t\tlatest[i+1] = latest[i] // shift by +1.\n\t}\n\tlatest[0] = act\n```\n\nUpdating the \"latest\" array for viewing at gno.land/r/demo/banktest: (w/ trailing colon).\n\n```go\n\t// return if any.\n\tif returnAmount \u003e 0 {\n```\n\nIf the user requested the return of coins...\n\n```go\n\t\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n```\n\nuse a std.Banker instance to return any deposited coins to the original sender.\n\n```go\n\t\tpkgaddr := std.GetOrigPkgAddr()\n\t\t// TODO: use std.Coins constructors, this isn't generally safe.\n\t\tbanker.SendCoins(pkgaddr, caller, send)\n\t\treturn \"returned!\"\n```\n\nNotice that each realm package has an associated Cosmos address.\n\n\nFinally, the results are rendered via an ABCI query call when you visit [/r/demo/banktest:](/r/demo/banktest:).\n\n```go\nfunc Render(path string) string {\n\t// get realm coins.\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(std.GetOrigPkgAddr())\n\n\t// render\n\tres := \"\"\n\tres += \"## recent activity\\n\"\n\tres += \"\\n\"\n\tfor _, act := range latest {\n\t\tif act == nil {\n\t\t\tbreak\n\t\t}\n\t\tres += \" * \" + act.String() + \"\\n\"\n\t}\n\tres += \"\\n\"\n\tres += \"## total deposits\\n\"\n\tres += coins.String()\n\treturn res\n}\n```\n\nYou can call this contract yourself, by vistiing [/r/demo/banktest](/r/demo/banktest) and the [quickstart guide](/r/demo/boards:testboard/4).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"iZX/llZlNTdZMLv1goCTgK2bWqzT8enlTq56wMTCpVxJGA0BTvuEM5Nnt9vrnlG6Taqj2GuTrmEnJBkDFTmt9g=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","TASK: Describe in your words","Describe in an essay (250+ words), on your favorite medium, why you are interested in gno.land and gnolang.\n\nReply here with a URL link to your written piece as a comment, for rewards.\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"4HBNtrta8HdeHj4JTN56PBTRK8GOe31NMRRXDiyYtjozuyRdWfOGEsGjGgHWcoBUJq6DepBgD4FetdqfhZ6TNQ=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Getting Started","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm\nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n\n\n## Build `gnokey`, create your account, and interact with Gno.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### Build `gnokey`.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake\n```\n\n### Generate a seed/mnemonic code.\n\n```bash\n./build/gnokey generate\n```\n\nNOTE: You can generate 24 words with any good bip39 generator.\n\n### Create a new account using your mnemonic.\n\n```bash\n./build/gnokey add KEYNAME --recover\n```\n\nNOTE: `KEYNAME` is your key identifier, and should be changed.\n\n### Verify that you can see your account locally.\n\n```bash\n./build/gnokey list\n```\n\n## Interact with the blockchain:\n\n### Get your current balance, account number, and sequence number.\n\n```bash\n./build/gnokey query auth/accounts/ACCOUNT_ADDR --remote %%REMOTE%%\n```\n\nNOTE: you can retrieve your `ACCOUNT_ADDR` with `./build/gnokey list`.\n\n### Acquire testnet tokens using the official faucet.\n\nGo to https://gno.land/faucet\n\n### Create a board with a smart contract call.\n\nNOTE: `BOARDNAME` will be the slug of the board, and should be changed.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateBoard\" --args \"BOARDNAME\" --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateBoard\n\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards.GetBoardIDFromName(\\\"BOARDNAME\\\")\" --remote %%REMOTE%%\n```\n\n### Create a post of a board with a smart contract call.\n\nNOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateThread\" --args BOARD_ID --args \"Hello gno.land\" --args\\#file \"./examples/gno.land/r/demo/boards/example_post.md\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateThread\n\n### Create a comment to a post.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateReply\" --args \"BOARD_ID\" --args \"1\" --args \"1\" --args \"Nice to meet you too.\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateReply\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:BOARDNAME/1\" --remote %%REMOTE%%\n```\n\n### Render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:gnolang` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:gnolang\"\n```\n\n## Starting a local `gnoland` node:\n\n### Add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mneonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### Start `gnoland` node.\n\n```bash\n./build/gnoland\n```\n\nNOTE: This can be reset with `make reset`\n\n### Publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n\n### Publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 300000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post1","First post","Lorem Ipsum","2022-05-20T13:17:22Z","","tag1,tag2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}}
+{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post2","Second post","Lorem Ipsum","2022-05-20T13:17:23Z","","tag1,tag3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}}
\ No newline at end of file
diff --git a/gno.land/pkg/gnoclient/README.md b/gno.land/pkg/gnoclient/README.md
index a2f00895dbd..4b3854b1bcc 100644
--- a/gno.land/pkg/gnoclient/README.md
+++ b/gno.land/pkg/gnoclient/README.md
@@ -1,4 +1,4 @@
-# Gno.land Go Client
+# gno.land Go Client
The gno.land Go client is a dedicated library for interacting seamlessly with the gno.land RPC API.
This library simplifies the process of querying or sending transactions to the gno.land RPC API and interpreting the responses.
@@ -18,4 +18,3 @@ The roadmap for the gno.land Go client includes:
- **Initial Development:** Kickstart the development specifically for gno.land. Subsequently, transition the generic functionalities to other modules like `tm2`, `gnovm`, `gnosdk`.
- **Integration:** Begin incorporating this library within various components such as `gno.land/cmd/*` and other external clients, including `gnoblog-client`, the Discord community faucet bot, and [GnoMobile](https://github.com/gnolang/gnomobile).
- **Enhancements:** Once the generic client establishes a robust foundation, we aim to utilize code generation for contracts. This will streamline the creation of type-safe, contract-specific clients.
-
diff --git a/gno.land/pkg/gnoclient/client_queries.go b/gno.land/pkg/gnoclient/client_queries.go
index 9d9d7305116..2e09842ae31 100644
--- a/gno.land/pkg/gnoclient/client_queries.go
+++ b/gno.land/pkg/gnoclient/client_queries.go
@@ -31,7 +31,7 @@ func (c *Client) Query(cfg QueryCfg) (*ctypes.ResultABCIQuery, error) {
}
if qres.Response.Error != nil {
- return qres, errors.Wrap(qres.Response.Error, "deliver transaction failed: log:%s", qres.Response.Log)
+ return qres, errors.Wrapf(qres.Response.Error, "deliver transaction failed: log:%s", qres.Response.Log)
}
return qres, nil
@@ -97,7 +97,7 @@ func (c *Client) Render(pkgPath string, args string) (string, *ctypes.ResultABCI
return "", nil, errors.Wrap(err, "query render")
}
if qres.Response.Error != nil {
- return "", nil, errors.Wrap(qres.Response.Error, "Render failed: log:%s", qres.Response.Log)
+ return "", nil, errors.Wrapf(qres.Response.Error, "Render failed: log:%s", qres.Response.Log)
}
return string(qres.Response.Data), qres, nil
@@ -120,7 +120,7 @@ func (c *Client) QEval(pkgPath string, expression string) (string, *ctypes.Resul
return "", nil, errors.Wrap(err, "query qeval")
}
if qres.Response.Error != nil {
- return "", nil, errors.Wrap(qres.Response.Error, "QEval failed: log:%s", qres.Response.Log)
+ return "", nil, errors.Wrapf(qres.Response.Error, "QEval failed: log:%s", qres.Response.Log)
}
return string(qres.Response.Data), qres, nil
diff --git a/gno.land/pkg/gnoclient/client_test.go b/gno.land/pkg/gnoclient/client_test.go
index d7795f918bf..54a15420a66 100644
--- a/gno.land/pkg/gnoclient/client_test.go
+++ b/gno.land/pkg/gnoclient/client_test.go
@@ -1,13 +1,17 @@
package gnoclient
import (
+ "errors"
"testing"
+ "github.com/gnolang/gno/tm2/pkg/amino"
+ abciErrors "github.com/gnolang/gno/tm2/pkg/bft/abci/example/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
"github.com/gnolang/gno/gno.land/pkg/sdk/vm"
+ "github.com/gnolang/gno/gnovm"
abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types"
"github.com/gnolang/gno/tm2/pkg/bft/types"
@@ -17,7 +21,7 @@ import (
"github.com/gnolang/gno/tm2/pkg/std"
)
-var testGasFee = ugnot.ValueString(10000)
+var testGasFee = ugnot.ValueString(10_000)
func TestRender(t *testing.T) {
t.Parallel()
@@ -115,7 +119,13 @@ func TestCallSingle(t *testing.T) {
res, err := client.Call(cfg, msg...)
assert.NoError(t, err)
require.NotNil(t, res)
- assert.Equal(t, string(res.DeliverTx.Data), "it works!")
+ expected := "it works!"
+ assert.Equal(t, string(res.DeliverTx.Data), expected)
+
+ res, err = callSigningSeparately(t, client, cfg, msg...)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ assert.Equal(t, string(res.DeliverTx.Data), expected)
}
func TestCallMultiple(t *testing.T) {
@@ -192,6 +202,10 @@ func TestCallMultiple(t *testing.T) {
res, err := client.Call(cfg, msg...)
assert.NoError(t, err)
assert.NotNil(t, res)
+
+ res, err = callSigningSeparately(t, client, cfg, msg...)
+ assert.NoError(t, err)
+ assert.NotNil(t, res)
}
func TestCallErrors(t *testing.T) {
@@ -642,8 +656,8 @@ func main() {
msg := vm.MsgRun{
Caller: caller.GetAddress(),
- Package: &std.MemPackage{
- Files: []*std.MemFile{
+ Package: &gnovm.MemPackage{
+ Files: []*gnovm.MemFile{
{
Name: "main.gno",
Body: fileBody,
@@ -656,7 +670,13 @@ func main() {
res, err := client.Run(cfg, msg)
assert.NoError(t, err)
require.NotNil(t, res)
- assert.Equal(t, "hi gnoclient!\n", string(res.DeliverTx.Data))
+ expected := "hi gnoclient!\n"
+ assert.Equal(t, expected, string(res.DeliverTx.Data))
+
+ res, err = runSigningSeparately(t, client, cfg, msg)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ assert.Equal(t, expected, string(res.DeliverTx.Data))
}
func TestRunMultiple(t *testing.T) {
@@ -713,8 +733,8 @@ func main() {
msg1 := vm.MsgRun{
Caller: caller.GetAddress(),
- Package: &std.MemPackage{
- Files: []*std.MemFile{
+ Package: &gnovm.MemPackage{
+ Files: []*gnovm.MemFile{
{
Name: "main1.gno",
Body: fileBody,
@@ -726,8 +746,8 @@ func main() {
msg2 := vm.MsgRun{
Caller: caller.GetAddress(),
- Package: &std.MemPackage{
- Files: []*std.MemFile{
+ Package: &gnovm.MemPackage{
+ Files: []*gnovm.MemFile{
{
Name: "main2.gno",
Body: fileBody,
@@ -740,7 +760,13 @@ func main() {
res, err := client.Run(cfg, msg1, msg2)
assert.NoError(t, err)
require.NotNil(t, res)
- assert.Equal(t, "hi gnoclient!\nhi gnoclient!\n", string(res.DeliverTx.Data))
+ expected := "hi gnoclient!\nhi gnoclient!\n"
+ assert.Equal(t, expected, string(res.DeliverTx.Data))
+
+ res, err = runSigningSeparately(t, client, cfg, msg1, msg2)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ assert.Equal(t, expected, string(res.DeliverTx.Data))
}
func TestRunErrors(t *testing.T) {
@@ -772,10 +798,10 @@ func TestRunErrors(t *testing.T) {
msgs: []vm.MsgRun{
{
Caller: mockAddress,
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: "",
Path: "",
- Files: []*std.MemFile{
+ Files: []*gnovm.MemFile{
{
Name: "file1.gno",
Body: "",
@@ -819,10 +845,10 @@ func TestRunErrors(t *testing.T) {
msgs: []vm.MsgRun{
{
Caller: mockAddress,
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: "",
Path: "",
- Files: []*std.MemFile{
+ Files: []*gnovm.MemFile{
{
Name: "file1.gno",
Body: "",
@@ -850,10 +876,10 @@ func TestRunErrors(t *testing.T) {
msgs: []vm.MsgRun{
{
Caller: mockAddress,
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: "",
Path: "",
- Files: []*std.MemFile{
+ Files: []*gnovm.MemFile{
{
Name: "file1.gno",
Body: "",
@@ -881,10 +907,10 @@ func TestRunErrors(t *testing.T) {
msgs: []vm.MsgRun{
{
Caller: mockAddress,
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: "",
Path: "",
- Files: []*std.MemFile{
+ Files: []*gnovm.MemFile{
{
Name: "file1.gno",
Body: "",
@@ -921,7 +947,7 @@ func TestRunErrors(t *testing.T) {
msgs: []vm.MsgRun{
{
Caller: mockAddress,
- Package: &std.MemPackage{Name: "", Path: " "},
+ Package: &gnovm.MemPackage{Name: "", Path: " "},
Send: nil,
},
},
@@ -971,10 +997,10 @@ func TestAddPackageErrors(t *testing.T) {
msgs: []vm.MsgAddPackage{
{
Creator: mockAddress,
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: "",
Path: "",
- Files: []*std.MemFile{
+ Files: []*gnovm.MemFile{
{
Name: "file1.gno",
Body: "",
@@ -1018,10 +1044,10 @@ func TestAddPackageErrors(t *testing.T) {
msgs: []vm.MsgAddPackage{
{
Creator: mockAddress,
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: "",
Path: "",
- Files: []*std.MemFile{
+ Files: []*gnovm.MemFile{
{
Name: "file1.gno",
Body: "",
@@ -1049,10 +1075,10 @@ func TestAddPackageErrors(t *testing.T) {
msgs: []vm.MsgAddPackage{
{
Creator: mockAddress,
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: "",
Path: "",
- Files: []*std.MemFile{
+ Files: []*gnovm.MemFile{
{
Name: "file1.gno",
Body: "",
@@ -1080,10 +1106,10 @@ func TestAddPackageErrors(t *testing.T) {
msgs: []vm.MsgAddPackage{
{
Creator: mockAddress,
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: "",
Path: "",
- Files: []*std.MemFile{
+ Files: []*gnovm.MemFile{
{
Name: "file1.gno",
Body: "",
@@ -1120,7 +1146,7 @@ func TestAddPackageErrors(t *testing.T) {
msgs: []vm.MsgAddPackage{
{
Creator: mockAddress,
- Package: &std.MemPackage{Name: "", Path: ""},
+ Package: &gnovm.MemPackage{Name: "", Path: ""},
Deposit: nil,
},
},
@@ -1326,3 +1352,217 @@ func TestLatestBlockHeightErrors(t *testing.T) {
})
}
}
+
+// The same as client.Call, but test signing separately
+func callSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...vm.MsgCall) (*ctypes.ResultBroadcastTxCommit, error) {
+ t.Helper()
+ tx, err := NewCallTx(cfg, msgs...)
+ assert.NoError(t, err)
+ require.NotNil(t, tx)
+ signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber)
+ assert.NoError(t, err)
+ require.NotNil(t, signedTx)
+ res, err := client.BroadcastTxCommit(signedTx)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res, nil
+}
+
+// The same as client.Run, but test signing separately
+func runSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...vm.MsgRun) (*ctypes.ResultBroadcastTxCommit, error) {
+ t.Helper()
+ tx, err := NewRunTx(cfg, msgs...)
+ assert.NoError(t, err)
+ require.NotNil(t, tx)
+ signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber)
+ assert.NoError(t, err)
+ require.NotNil(t, signedTx)
+ res, err := client.BroadcastTxCommit(signedTx)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res, nil
+}
+
+// The same as client.Send, but test signing separately
+func sendSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...bank.MsgSend) (*ctypes.ResultBroadcastTxCommit, error) {
+ t.Helper()
+ tx, err := NewSendTx(cfg, msgs...)
+ assert.NoError(t, err)
+ require.NotNil(t, tx)
+ signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber)
+ assert.NoError(t, err)
+ require.NotNil(t, signedTx)
+ res, err := client.BroadcastTxCommit(signedTx)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res, nil
+}
+
+// The same as client.AddPackage, but test signing separately
+func addPackageSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*ctypes.ResultBroadcastTxCommit, error) {
+ t.Helper()
+ tx, err := NewAddPackageTx(cfg, msgs...)
+ assert.NoError(t, err)
+ require.NotNil(t, tx)
+ signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber)
+ assert.NoError(t, err)
+ require.NotNil(t, signedTx)
+ res, err := client.BroadcastTxCommit(signedTx)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res, nil
+}
+
+func TestClient_EstimateGas(t *testing.T) {
+ t.Parallel()
+
+ t.Run("RPC client not set", func(t *testing.T) {
+ t.Parallel()
+
+ c := &Client{
+ RPCClient: nil, // not set
+ }
+
+ estimate, err := c.EstimateGas(&std.Tx{})
+
+ assert.Zero(t, estimate)
+ assert.ErrorIs(t, err, ErrMissingRPCClient)
+ })
+
+ t.Run("unsuccessful query, rpc error", func(t *testing.T) {
+ t.Parallel()
+
+ var (
+ rpcErr = errors.New("rpc error")
+ mockRPCClient = &mockRPCClient{
+ abciQuery: func(path string, data []byte) (*ctypes.ResultABCIQuery, error) {
+ require.Equal(t, simulatePath, path)
+
+ var tx std.Tx
+
+ require.NoError(t, amino.Unmarshal(data, &tx))
+
+ return nil, rpcErr
+ },
+ }
+ )
+
+ c := &Client{
+ RPCClient: mockRPCClient,
+ }
+
+ estimate, err := c.EstimateGas(&std.Tx{})
+
+ assert.Zero(t, estimate)
+ assert.ErrorIs(t, err, rpcErr)
+ })
+
+ t.Run("unsuccessful query, process error", func(t *testing.T) {
+ t.Parallel()
+
+ var (
+ response = &ctypes.ResultABCIQuery{
+ Response: abci.ResponseQuery{
+ ResponseBase: abci.ResponseBase{
+ Error: abciErrors.UnknownError{},
+ },
+ },
+ }
+ mockRPCClient = &mockRPCClient{
+ abciQuery: func(path string, data []byte) (*ctypes.ResultABCIQuery, error) {
+ require.Equal(t, simulatePath, path)
+
+ var tx std.Tx
+
+ require.NoError(t, amino.Unmarshal(data, &tx))
+
+ return response, nil
+ },
+ }
+ )
+
+ c := &Client{
+ RPCClient: mockRPCClient,
+ }
+
+ estimate, err := c.EstimateGas(&std.Tx{})
+
+ assert.Zero(t, estimate)
+ assert.ErrorIs(t, err, abciErrors.UnknownError{})
+ })
+
+ t.Run("invalid response format", func(t *testing.T) {
+ t.Parallel()
+
+ var (
+ response = &ctypes.ResultABCIQuery{
+ Response: abci.ResponseQuery{
+ Value: []byte("totally valid amino"),
+ },
+ }
+ mockRPCClient = &mockRPCClient{
+ abciQuery: func(path string, data []byte) (*ctypes.ResultABCIQuery, error) {
+ require.Equal(t, simulatePath, path)
+
+ var tx std.Tx
+
+ require.NoError(t, amino.Unmarshal(data, &tx))
+
+ return response, nil
+ },
+ }
+ )
+
+ c := &Client{
+ RPCClient: mockRPCClient,
+ }
+
+ estimate, err := c.EstimateGas(&std.Tx{})
+
+ assert.Zero(t, estimate)
+ assert.ErrorContains(t, err, "unable to unmarshal gas estimation response")
+ })
+
+ t.Run("valid gas estimation", func(t *testing.T) {
+ t.Parallel()
+
+ var (
+ gasUsed = int64(100000)
+ deliverResp = &abci.ResponseDeliverTx{
+ GasUsed: gasUsed,
+ }
+ )
+
+ // Encode the response
+ encodedResp, err := amino.Marshal(deliverResp)
+ require.NoError(t, err)
+
+ var (
+ response = &ctypes.ResultABCIQuery{
+ Response: abci.ResponseQuery{
+ Value: encodedResp, // valid amino binary
+ },
+ }
+ mockRPCClient = &mockRPCClient{
+ abciQuery: func(path string, data []byte) (*ctypes.ResultABCIQuery, error) {
+ require.Equal(t, simulatePath, path)
+
+ var tx std.Tx
+
+ require.NoError(t, amino.Unmarshal(data, &tx))
+
+ return response, nil
+ },
+ }
+ )
+
+ c := &Client{
+ RPCClient: mockRPCClient,
+ }
+
+ estimate, err := c.EstimateGas(&std.Tx{})
+
+ require.NoError(t, err)
+ assert.Equal(t, gasUsed, estimate)
+ })
+}
diff --git a/gno.land/pkg/gnoclient/client_txs.go b/gno.land/pkg/gnoclient/client_txs.go
index c113ea21944..ab520eceda1 100644
--- a/gno.land/pkg/gnoclient/client_txs.go
+++ b/gno.land/pkg/gnoclient/client_txs.go
@@ -1,8 +1,11 @@
package gnoclient
import (
+ "fmt"
+
"github.com/gnolang/gno/gno.land/pkg/sdk/vm"
"github.com/gnolang/gno/tm2/pkg/amino"
+ abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types"
"github.com/gnolang/gno/tm2/pkg/errors"
"github.com/gnolang/gno/tm2/pkg/sdk/bank"
@@ -16,6 +19,8 @@ var (
ErrMissingRPCClient = errors.New("missing RPCClient")
)
+const simulatePath = ".app/simulate"
+
// BaseTxCfg defines the base transaction configuration, shared by all message types
type BaseTxCfg struct {
GasFee string // Gas fee
@@ -35,6 +40,16 @@ func (c *Client) Call(cfg BaseTxCfg, msgs ...vm.MsgCall) (*ctypes.ResultBroadcas
return nil, err
}
+ tx, err := NewCallTx(cfg, msgs...)
+ if err != nil {
+ return nil, err
+ }
+ return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber)
+}
+
+// NewCallTx makes an unsigned transaction from one or more MsgCall.
+// The Caller field must be set.
+func NewCallTx(cfg BaseTxCfg, msgs ...vm.MsgCall) (*std.Tx, error) {
// Validate base transaction config
if err := cfg.validateBaseTxConfig(); err != nil {
return nil, err
@@ -57,14 +72,12 @@ func (c *Client) Call(cfg BaseTxCfg, msgs ...vm.MsgCall) (*ctypes.ResultBroadcas
}
// Pack transaction
- tx := std.Tx{
+ return &std.Tx{
Msgs: vmMsgs,
Fee: std.NewFee(cfg.GasWanted, gasFeeCoins),
Signatures: nil,
Memo: cfg.Memo,
- }
-
- return c.signAndBroadcastTxCommit(tx, cfg.AccountNumber, cfg.SequenceNumber)
+ }, nil
}
// Run executes one or more MsgRun calls on the blockchain
@@ -77,6 +90,16 @@ func (c *Client) Run(cfg BaseTxCfg, msgs ...vm.MsgRun) (*ctypes.ResultBroadcastT
return nil, err
}
+ tx, err := NewRunTx(cfg, msgs...)
+ if err != nil {
+ return nil, err
+ }
+ return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber)
+}
+
+// NewRunTx makes an unsigned transaction from one or more MsgRun.
+// The Caller field must be set.
+func NewRunTx(cfg BaseTxCfg, msgs ...vm.MsgRun) (*std.Tx, error) {
// Validate base transaction config
if err := cfg.validateBaseTxConfig(); err != nil {
return nil, err
@@ -99,14 +122,12 @@ func (c *Client) Run(cfg BaseTxCfg, msgs ...vm.MsgRun) (*ctypes.ResultBroadcastT
}
// Pack transaction
- tx := std.Tx{
+ return &std.Tx{
Msgs: vmMsgs,
Fee: std.NewFee(cfg.GasWanted, gasFeeCoins),
Signatures: nil,
Memo: cfg.Memo,
- }
-
- return c.signAndBroadcastTxCommit(tx, cfg.AccountNumber, cfg.SequenceNumber)
+ }, nil
}
// Send executes one or more MsgSend calls on the blockchain
@@ -119,6 +140,16 @@ func (c *Client) Send(cfg BaseTxCfg, msgs ...bank.MsgSend) (*ctypes.ResultBroadc
return nil, err
}
+ tx, err := NewSendTx(cfg, msgs...)
+ if err != nil {
+ return nil, err
+ }
+ return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber)
+}
+
+// NewSendTx makes an unsigned transaction from one or more MsgSend.
+// The FromAddress field must be set.
+func NewSendTx(cfg BaseTxCfg, msgs ...bank.MsgSend) (*std.Tx, error) {
// Validate base transaction config
if err := cfg.validateBaseTxConfig(); err != nil {
return nil, err
@@ -141,14 +172,12 @@ func (c *Client) Send(cfg BaseTxCfg, msgs ...bank.MsgSend) (*ctypes.ResultBroadc
}
// Pack transaction
- tx := std.Tx{
+ return &std.Tx{
Msgs: vmMsgs,
Fee: std.NewFee(cfg.GasWanted, gasFeeCoins),
Signatures: nil,
Memo: cfg.Memo,
- }
-
- return c.signAndBroadcastTxCommit(tx, cfg.AccountNumber, cfg.SequenceNumber)
+ }, nil
}
// AddPackage executes one or more AddPackage calls on the blockchain
@@ -161,6 +190,16 @@ func (c *Client) AddPackage(cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*ctypes.Re
return nil, err
}
+ tx, err := NewAddPackageTx(cfg, msgs...)
+ if err != nil {
+ return nil, err
+ }
+ return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber)
+}
+
+// NewAddPackageTx makes an unsigned transaction from one or more MsgAddPackage.
+// The Creator field must be set.
+func NewAddPackageTx(cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*std.Tx, error) {
// Validate base transaction config
if err := cfg.validateBaseTxConfig(); err != nil {
return nil, err
@@ -183,18 +222,29 @@ func (c *Client) AddPackage(cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*ctypes.Re
}
// Pack transaction
- tx := std.Tx{
+ return &std.Tx{
Msgs: vmMsgs,
Fee: std.NewFee(cfg.GasWanted, gasFeeCoins),
Signatures: nil,
Memo: cfg.Memo,
- }
-
- return c.signAndBroadcastTxCommit(tx, cfg.AccountNumber, cfg.SequenceNumber)
+ }, nil
}
// signAndBroadcastTxCommit signs a transaction and broadcasts it, returning the result
func (c *Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumber uint64) (*ctypes.ResultBroadcastTxCommit, error) {
+ signedTx, err := c.SignTx(tx, accountNumber, sequenceNumber)
+ if err != nil {
+ return nil, err
+ }
+ return c.BroadcastTxCommit(signedTx)
+}
+
+// SignTx signs a transaction and returns a signed tx ready for broadcasting.
+// If accountNumber or sequenceNumber is 0 then query the blockchain for the value.
+func (c *Client) SignTx(tx std.Tx, accountNumber, sequenceNumber uint64) (*std.Tx, error) {
+ if err := c.validateSigner(); err != nil {
+ return nil, err
+ }
caller, err := c.Signer.Info()
if err != nil {
return nil, err
@@ -218,7 +268,15 @@ func (c *Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumb
if err != nil {
return nil, errors.Wrap(err, "sign")
}
+ return signedTx, nil
+}
+// BroadcastTxCommit marshals and broadcasts the signed transaction, returning the result.
+// If the result has a delivery error, then return a wrapped error.
+func (c *Client) BroadcastTxCommit(signedTx *std.Tx) (*ctypes.ResultBroadcastTxCommit, error) {
+ if err := c.validateRPCClient(); err != nil {
+ return nil, err
+ }
bz, err := amino.Marshal(signedTx)
if err != nil {
return nil, errors.Wrap(err, "marshaling tx binary bytes")
@@ -230,13 +288,53 @@ func (c *Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumb
}
if bres.CheckTx.IsErr() {
- return bres, errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log)
+ return bres, errors.Wrapf(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log)
}
if bres.DeliverTx.IsErr() {
- return bres, errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log)
+ return bres, errors.Wrapf(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log)
}
return bres, nil
}
-// TODO: Add more functionality, examples, and unit tests.
+// EstimateGas returns the least amount of gas required
+// for the transaction to go through on the chain (minimum gas wanted).
+// The estimation process assumes the transaction is properly signed
+func (c *Client) EstimateGas(tx *std.Tx) (int64, error) {
+ // Make sure the RPC client is set
+ if err := c.validateRPCClient(); err != nil {
+ return 0, err
+ }
+
+ // Prepare the transaction.
+ // The transaction needs to be amino-binary encoded
+ // in order to be estimated
+ encodedTx, err := amino.Marshal(tx)
+ if err != nil {
+ return 0, fmt.Errorf("unable to marshal tx: %w", err)
+ }
+
+ // Perform the simulation query
+ resp, err := c.RPCClient.ABCIQuery(simulatePath, encodedTx)
+ if err != nil {
+ return 0, fmt.Errorf("unable to perform ABCI query: %w", err)
+ }
+
+ // Extract the query response
+ if err = resp.Response.Error; err != nil {
+ return 0, fmt.Errorf("error encountered during ABCI query: %w", err)
+ }
+
+ var deliverTx abci.ResponseDeliverTx
+ if err = amino.Unmarshal(resp.Response.Value, &deliverTx); err != nil {
+ return 0, fmt.Errorf("unable to unmarshal gas estimation response: %w", err)
+ }
+
+ if err = deliverTx.Error; err != nil {
+ return 0, fmt.Errorf("error encountered during gas estimation: %w", err)
+ }
+
+ // Return the actual value returned by the node
+ // for executing the transaction
+ return deliverTx.GasUsed, nil
+}
diff --git a/gno.land/pkg/gnoclient/integration_test.go b/gno.land/pkg/gnoclient/integration_test.go
index f2e5026aa9a..bfcaaec999e 100644
--- a/gno.land/pkg/gnoclient/integration_test.go
+++ b/gno.land/pkg/gnoclient/integration_test.go
@@ -1,28 +1,36 @@
package gnoclient
import (
+ "path/filepath"
"testing"
"github.com/gnolang/gno/gnovm/pkg/gnolang"
- "github.com/gnolang/gno/tm2/pkg/sdk/bank"
- "github.com/gnolang/gno/tm2/pkg/std"
-
+ "github.com/gnolang/gno/gno.land/pkg/gnoland"
"github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
"github.com/gnolang/gno/gno.land/pkg/integration"
"github.com/gnolang/gno/gno.land/pkg/sdk/vm"
+ "github.com/gnolang/gno/gnovm"
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client"
"github.com/gnolang/gno/tm2/pkg/crypto"
"github.com/gnolang/gno/tm2/pkg/crypto/keys"
"github.com/gnolang/gno/tm2/pkg/log"
+ "github.com/gnolang/gno/tm2/pkg/sdk/bank"
+ "github.com/gnolang/gno/tm2/pkg/std"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCallSingle_Integration(t *testing.T) {
- // Set up in-memory node
- config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
+ // Setup packages
+ rootdir := gnoenv.RootDir()
+ config := integration.TestingMinimalNodeConfig(gnoenv.RootDir())
+ meta := loadpkgs(t, rootdir, "gno.land/r/demo/deep/very/deep")
+ state := config.Genesis.AppState.(gnoland.GnoGenesisState)
+ state.Txs = append(state.Txs, meta...)
+ config.Genesis.AppState = state
+
node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()
@@ -39,8 +47,8 @@ func TestCallSingle_Integration(t *testing.T) {
// Make Tx config
baseCfg := BaseTxCfg{
- GasFee: ugnot.ValueString(10000),
- GasWanted: 8000000,
+ GasFee: ugnot.ValueString(2100000),
+ GasWanted: 21000000,
AccountNumber: 0,
SequenceNumber: 0,
Memo: "",
@@ -66,11 +74,22 @@ func TestCallSingle_Integration(t *testing.T) {
got := string(res.DeliverTx.Data)
assert.Equal(t, expected, got)
+
+ res, err = callSigningSeparately(t, client, baseCfg, msg)
+ require.NoError(t, err)
+ got = string(res.DeliverTx.Data)
+ assert.Equal(t, expected, got)
}
func TestCallMultiple_Integration(t *testing.T) {
- // Set up in-memory node
- config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
+ // Setup packages
+ rootdir := gnoenv.RootDir()
+ config := integration.TestingMinimalNodeConfig(gnoenv.RootDir())
+ meta := loadpkgs(t, rootdir, "gno.land/r/demo/deep/very/deep")
+ state := config.Genesis.AppState.(gnoland.GnoGenesisState)
+ state.Txs = append(state.Txs, meta...)
+ config.Genesis.AppState = state
+
node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()
@@ -87,8 +106,8 @@ func TestCallMultiple_Integration(t *testing.T) {
// Make Tx config
baseCfg := BaseTxCfg{
- GasFee: ugnot.ValueString(10000),
- GasWanted: 8000000,
+ GasFee: ugnot.ValueString(2100000),
+ GasWanted: 21000000,
AccountNumber: 0,
SequenceNumber: 0,
Memo: "",
@@ -123,11 +142,16 @@ func TestCallMultiple_Integration(t *testing.T) {
got := string(res.DeliverTx.Data)
assert.Equal(t, expected, got)
+
+ res, err = callSigningSeparately(t, client, baseCfg, msg1, msg2)
+ require.NoError(t, err)
+ got = string(res.DeliverTx.Data)
+ assert.Equal(t, expected, got)
}
func TestSendSingle_Integration(t *testing.T) {
// Set up in-memory node
- config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
+ config := integration.TestingMinimalNodeConfig(gnoenv.RootDir())
node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()
@@ -144,8 +168,8 @@ func TestSendSingle_Integration(t *testing.T) {
// Make Tx config
baseCfg := BaseTxCfg{
- GasFee: ugnot.ValueString(10000),
- GasWanted: 8000000,
+ GasFee: ugnot.ValueString(2100000),
+ GasWanted: 21000000,
AccountNumber: 0,
SequenceNumber: 0,
Memo: "",
@@ -176,11 +200,22 @@ func TestSendSingle_Integration(t *testing.T) {
got := account.GetCoins()
assert.Equal(t, expected, got)
+
+ res, err = sendSigningSeparately(t, client, baseCfg, msg)
+ require.NoError(t, err)
+ assert.Equal(t, "", string(res.DeliverTx.Data))
+
+ // Get the new account balance
+ account, _, err = client.QueryAccount(toAddress)
+ require.NoError(t, err)
+ expected2 := std.Coins{{Denom: ugnot.Denom, Amount: int64(2 * amount)}}
+ got = account.GetCoins()
+ assert.Equal(t, expected2, got)
}
func TestSendMultiple_Integration(t *testing.T) {
// Set up in-memory node
- config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
+ config := integration.TestingMinimalNodeConfig(gnoenv.RootDir())
node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()
@@ -197,8 +232,8 @@ func TestSendMultiple_Integration(t *testing.T) {
// Make Tx config
baseCfg := BaseTxCfg{
- GasFee: ugnot.ValueString(10000),
- GasWanted: 8000000,
+ GasFee: ugnot.ValueString(2100000),
+ GasWanted: 21000000,
AccountNumber: 0,
SequenceNumber: 0,
Memo: "",
@@ -237,12 +272,30 @@ func TestSendMultiple_Integration(t *testing.T) {
got := account.GetCoins()
assert.Equal(t, expected, got)
+
+ res, err = sendSigningSeparately(t, client, baseCfg, msg1, msg2)
+ require.NoError(t, err)
+ assert.Equal(t, "", string(res.DeliverTx.Data))
+
+ // Get the new account balance
+ account, _, err = client.QueryAccount(toAddress)
+ require.NoError(t, err)
+ expected2 := std.Coins{{Denom: ugnot.Denom, Amount: int64(2 * (amount1 + amount2))}}
+ got = account.GetCoins()
+ assert.Equal(t, expected2, got)
}
// Run tests
func TestRunSingle_Integration(t *testing.T) {
+ // Setup packages
+ rootdir := gnoenv.RootDir()
+ config := integration.TestingMinimalNodeConfig(gnoenv.RootDir())
+ meta := loadpkgs(t, rootdir, "gno.land/p/demo/ufmt", "gno.land/r/demo/tests")
+ state := config.Genesis.AppState.(gnoland.GnoGenesisState)
+ state.Txs = append(state.Txs, meta...)
+ config.Genesis.AppState = state
+
// Set up in-memory node
- config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()
@@ -258,8 +311,8 @@ func TestRunSingle_Integration(t *testing.T) {
// Make Tx config
baseCfg := BaseTxCfg{
- GasFee: ugnot.ValueString(10000),
- GasWanted: 8000000,
+ GasFee: ugnot.ValueString(2100000),
+ GasWanted: 21000000,
AccountNumber: 0,
SequenceNumber: 0,
Memo: "",
@@ -284,9 +337,9 @@ func main() {
// Make Msg configs
msg := vm.MsgRun{
Caller: caller.GetAddress(),
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: "main",
- Files: []*std.MemFile{
+ Files: []*gnovm.MemFile{
{
Name: "main.gno",
Body: fileBody,
@@ -300,12 +353,27 @@ func main() {
assert.NoError(t, err)
require.NotNil(t, res)
assert.Equal(t, string(res.DeliverTx.Data), "- before: 0\n- after: 10\n")
+
+ res, err = runSigningSeparately(t, client, baseCfg, msg)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ assert.Equal(t, string(res.DeliverTx.Data), "- before: 10\n- after: 20\n")
}
// Run tests
func TestRunMultiple_Integration(t *testing.T) {
// Set up in-memory node
- config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
+ rootdir := gnoenv.RootDir()
+ config := integration.TestingMinimalNodeConfig(rootdir)
+ meta := loadpkgs(t, rootdir,
+ "gno.land/p/demo/ufmt",
+ "gno.land/r/demo/tests",
+ "gno.land/r/demo/deep/very/deep",
+ )
+ state := config.Genesis.AppState.(gnoland.GnoGenesisState)
+ state.Txs = append(state.Txs, meta...)
+ config.Genesis.AppState = state
+
node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()
@@ -321,8 +389,8 @@ func TestRunMultiple_Integration(t *testing.T) {
// Make Tx config
baseCfg := BaseTxCfg{
- GasFee: ugnot.ValueString(10000),
- GasWanted: 8000000,
+ GasFee: ugnot.ValueString(2300000),
+ GasWanted: 23000000,
AccountNumber: 0,
SequenceNumber: 0,
Memo: "",
@@ -356,9 +424,9 @@ func main() {
// Make Msg configs
msg1 := vm.MsgRun{
Caller: caller.GetAddress(),
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: "main",
- Files: []*std.MemFile{
+ Files: []*gnovm.MemFile{
{
Name: "main.gno",
Body: fileBody1,
@@ -369,9 +437,9 @@ func main() {
}
msg2 := vm.MsgRun{
Caller: caller.GetAddress(),
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: "main",
- Files: []*std.MemFile{
+ Files: []*gnovm.MemFile{
{
Name: "main.gno",
Body: fileBody2,
@@ -387,11 +455,17 @@ func main() {
assert.NoError(t, err)
require.NotNil(t, res)
assert.Equal(t, expected, string(res.DeliverTx.Data))
+
+ res, err = runSigningSeparately(t, client, baseCfg, msg1, msg2)
+ require.NoError(t, err)
+ require.NotNil(t, res)
+ expected2 := "- before: 10\n- after: 20\nhi gnoclient!\n"
+ assert.Equal(t, expected2, string(res.DeliverTx.Data))
}
func TestAddPackageSingle_Integration(t *testing.T) {
// Set up in-memory node
- config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
+ config := integration.TestingMinimalNodeConfig(gnoenv.RootDir())
node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()
@@ -408,8 +482,8 @@ func TestAddPackageSingle_Integration(t *testing.T) {
// Make Tx config
baseCfg := BaseTxCfg{
- GasFee: ugnot.ValueString(10000),
- GasWanted: 8000000,
+ GasFee: ugnot.ValueString(2100000),
+ GasWanted: 21000000,
AccountNumber: 0,
SequenceNumber: 0,
Memo: "",
@@ -431,10 +505,10 @@ func Echo(str string) string {
// Make Msg config
msg := vm.MsgAddPackage{
Creator: caller.GetAddress(),
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: "echo",
Path: deploymentPath,
- Files: []*std.MemFile{
+ Files: []*gnovm.MemFile{
{
Name: fileName,
Body: body,
@@ -460,11 +534,23 @@ func Echo(str string) string {
baseAcc, _, err := client.QueryAccount(gnolang.DerivePkgAddr(deploymentPath))
require.NoError(t, err)
assert.Equal(t, baseAcc.GetCoins(), deposit)
+
+ // Test signing separately (using a different deployment path)
+ deploymentPathB := "gno.land/p/demo/integration/test/echo2"
+ msg.Package.Path = deploymentPathB
+ _, err = addPackageSigningSeparately(t, client, baseCfg, msg)
+ assert.NoError(t, err)
+ query, err = client.Query(QueryCfg{
+ Path: "vm/qfile",
+ Data: []byte(deploymentPathB),
+ })
+ require.NoError(t, err)
+ assert.Equal(t, string(query.Response.Data), fileName)
}
func TestAddPackageMultiple_Integration(t *testing.T) {
// Set up in-memory node
- config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
+ config := integration.TestingMinimalNodeConfig(gnoenv.RootDir())
node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()
@@ -481,8 +567,8 @@ func TestAddPackageMultiple_Integration(t *testing.T) {
// Make Tx config
baseCfg := BaseTxCfg{
- GasFee: ugnot.ValueString(10000),
- GasWanted: 8000000,
+ GasFee: ugnot.ValueString(2100000),
+ GasWanted: 21000000,
AccountNumber: 0,
SequenceNumber: 0,
Memo: "",
@@ -501,7 +587,7 @@ func Echo(str string) string {
body2 := `package hello
func Hello(str string) string {
- return "Hello " + str + "!"
+ return "Hello " + str + "!"
}`
caller, err := client.Signer.Info()
@@ -509,10 +595,10 @@ func Hello(str string) string {
msg1 := vm.MsgAddPackage{
Creator: caller.GetAddress(),
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: "echo",
Path: deploymentPath1,
- Files: []*std.MemFile{
+ Files: []*gnovm.MemFile{
{
Name: "echo.gno",
Body: body1,
@@ -524,10 +610,10 @@ func Hello(str string) string {
msg2 := vm.MsgAddPackage{
Creator: caller.GetAddress(),
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: "hello",
Path: deploymentPath2,
- Files: []*std.MemFile{
+ Files: []*gnovm.MemFile{
{
Name: "gno.mod",
Body: "module gno.land/p/demo/integration/test/hello",
@@ -571,6 +657,27 @@ func Hello(str string) string {
baseAcc, _, err = client.QueryAccount(gnolang.DerivePkgAddr(deploymentPath2))
require.NoError(t, err)
assert.Equal(t, baseAcc.GetCoins(), deposit)
+
+ // Test signing separately (using a different deployment path)
+ deploymentPath1B := "gno.land/p/demo/integration/test/echo2"
+ deploymentPath2B := "gno.land/p/demo/integration/test/hello2"
+ msg1.Package.Path = deploymentPath1B
+ msg2.Package.Path = deploymentPath2B
+ _, err = addPackageSigningSeparately(t, client, baseCfg, msg1, msg2)
+ assert.NoError(t, err)
+ query, err = client.Query(QueryCfg{
+ Path: "vm/qfile",
+ Data: []byte(deploymentPath1B),
+ })
+ require.NoError(t, err)
+ assert.Equal(t, string(query.Response.Data), "echo.gno")
+ query, err = client.Query(QueryCfg{
+ Path: "vm/qfile",
+ Data: []byte(deploymentPath2B),
+ })
+ require.NoError(t, err)
+ assert.Contains(t, string(query.Response.Data), "hello.gno")
+ assert.Contains(t, string(query.Response.Data), "gno.mod")
}
// todo add more integration tests:
@@ -594,3 +701,24 @@ func newInMemorySigner(t *testing.T, chainid string) *SignerFromKeybase {
ChainID: chainid, // Chain ID for transaction signing
}
}
+
+func loadpkgs(t *testing.T, rootdir string, paths ...string) []gnoland.TxWithMetadata {
+ t.Helper()
+
+ loader := integration.NewPkgsLoader()
+ examplesDir := filepath.Join(rootdir, "examples")
+ for _, path := range paths {
+ path = filepath.Clean(path)
+ path = filepath.Join(examplesDir, path)
+ err := loader.LoadPackage(examplesDir, path, "")
+ require.NoErrorf(t, err, "`loadpkg` unable to load package(s) from %q: %s", path, err)
+ }
+ privKey, err := integration.GeneratePrivKeyFromMnemonic(integration.DefaultAccount_Seed, "", 0, 0)
+ require.NoError(t, err)
+
+ defaultFee := std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000)))
+
+ meta, err := loader.LoadPackages(privKey, defaultFee, nil)
+ require.NoError(t, err)
+ return meta
+}
diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go
index f4d353411f8..0826071b9f5 100644
--- a/gno.land/pkg/gnoland/app.go
+++ b/gno.land/pkg/gnoland/app.go
@@ -1,15 +1,19 @@
+// Package gnoland contains the bootstrapping code to launch a gno.land node.
package gnoland
import (
"fmt"
+ "io"
"log/slog"
"path/filepath"
"strconv"
+ "time"
"github.com/gnolang/gno/gno.land/pkg/sdk/vm"
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
"github.com/gnolang/gno/tm2/pkg/bft/config"
+ bft "github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/gnolang/gno/tm2/pkg/crypto"
dbm "github.com/gnolang/gno/tm2/pkg/db"
"github.com/gnolang/gno/tm2/pkg/events"
@@ -17,6 +21,7 @@ import (
"github.com/gnolang/gno/tm2/pkg/sdk"
"github.com/gnolang/gno/tm2/pkg/sdk/auth"
"github.com/gnolang/gno/tm2/pkg/sdk/bank"
+ "github.com/gnolang/gno/tm2/pkg/sdk/params"
"github.com/gnolang/gno/tm2/pkg/std"
"github.com/gnolang/gno/tm2/pkg/store"
"github.com/gnolang/gno/tm2/pkg/store/dbadapter"
@@ -25,48 +30,49 @@ import (
// Only goleveldb is supported for now.
_ "github.com/gnolang/gno/tm2/pkg/db/_tags"
_ "github.com/gnolang/gno/tm2/pkg/db/goleveldb"
- "github.com/gnolang/gno/tm2/pkg/db/memdb"
)
+// AppOptions contains the options to create the gno.land ABCI application.
type AppOptions struct {
- DB dbm.DB
- // `gnoRootDir` should point to the local location of the gno repository.
- // It serves as the gno equivalent of GOROOT.
- GnoRootDir string
- GenesisTxHandler GenesisTxHandler
- Logger *slog.Logger
- EventSwitch events.EventSwitch
- MaxCycles int64
- // Whether to cache the result of loading the standard libraries.
- // This is useful if you have to start many nodes, like in testing.
- // This disables loading existing packages; so it should only be used
- // on a fresh database.
- CacheStdlibLoad bool
+ DB dbm.DB // required
+ Logger *slog.Logger // required
+ EventSwitch events.EventSwitch // required
+ VMOutput io.Writer // optional
+ SkipGenesisVerification bool // default to verify genesis transactions
+ InitChainerConfig // options related to InitChainer
+ MinGasPrices string // optional
}
-func NewAppOptions() *AppOptions {
+// TestAppOptions provides a "ready" default [AppOptions] for use with
+// [NewAppWithOptions], using the provided db.
+func TestAppOptions(db dbm.DB) *AppOptions {
return &AppOptions{
- GenesisTxHandler: PanicOnFailingTxHandler,
- Logger: log.NewNoopLogger(),
- DB: memdb.NewMemDB(),
- GnoRootDir: gnoenv.RootDir(),
- EventSwitch: events.NilEventSwitch(),
+ DB: db,
+ Logger: log.NewNoopLogger(),
+ EventSwitch: events.NewEventSwitch(),
+ InitChainerConfig: InitChainerConfig{
+ GenesisTxResultHandler: PanicOnFailingTxResultHandler,
+ StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"),
+ CacheStdlibLoad: true,
+ },
+ SkipGenesisVerification: true,
}
}
-func (c *AppOptions) validate() error {
- if c.Logger == nil {
- return fmt.Errorf("no logger provided")
- }
-
- if c.DB == nil {
+func (c AppOptions) validate() error {
+ // Required fields
+ switch {
+ case c.DB == nil:
return fmt.Errorf("no db provided")
+ case c.Logger == nil:
+ return fmt.Errorf("no logger provided")
+ case c.EventSwitch == nil:
+ return fmt.Errorf("no event switch provided")
}
-
return nil
}
-// NewAppWithOptions creates the GnoLand application with specified options
+// NewAppWithOptions creates the gno.land application with specified options.
func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) {
if err := cfg.validate(); err != nil {
return nil, err
@@ -76,9 +82,13 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) {
mainKey := store.NewStoreKey("main")
baseKey := store.NewStoreKey("base")
+ // set sdk app options
+ var appOpts []func(*sdk.BaseApp)
+ if cfg.MinGasPrices != "" {
+ appOpts = append(appOpts, sdk.SetMinGasPrices(cfg.MinGasPrices))
+ }
// Create BaseApp.
- // TODO: Add a consensus based min gas prices for the node, by default it does not check
- baseApp := sdk.NewBaseApp("gnoland", cfg.Logger, cfg.DB, baseKey, mainKey)
+ baseApp := sdk.NewBaseApp("gnoland", cfg.Logger, cfg.DB, baseKey, mainKey, appOpts...)
baseApp.SetAppVersion("dev")
// Set mounts for BaseApp's MultiStore.
@@ -86,19 +96,23 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) {
baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB)
// Construct keepers.
- acctKpr := auth.NewAccountKeeper(mainKey, ProtoGnoAccount)
+ paramsKpr := params.NewParamsKeeper(mainKey, "vm")
+ acctKpr := auth.NewAccountKeeper(mainKey, paramsKpr, ProtoGnoAccount)
+ gpKpr := auth.NewGasPriceKeeper(mainKey)
bankKpr := bank.NewBankKeeper(acctKpr)
- // XXX: Embed this ?
- stdlibsDir := filepath.Join(cfg.GnoRootDir, "gnovm", "stdlibs")
- vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, stdlibsDir, cfg.MaxCycles)
+ vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, paramsKpr)
+ vmk.Output = cfg.VMOutput
// Set InitChainer
- baseApp.SetInitChainer(InitChainer(baseApp, acctKpr, bankKpr, cfg.GenesisTxHandler))
+ icc := cfg.InitChainerConfig
+ icc.baseApp = baseApp
+ icc.acctKpr, icc.bankKpr, icc.vmKpr, icc.paramsKpr, icc.gpKpr = acctKpr, bankKpr, vmk, paramsKpr, gpKpr
+ baseApp.SetInitChainer(icc.InitChainer)
// Set AnteHandler
authOptions := auth.AnteOptions{
- VerifyGenesisSignatures: false, // for development
+ VerifyGenesisSignatures: !cfg.SkipGenesisVerification,
}
authAnteHandler := auth.NewAnteHandler(
acctKpr, bankKpr, auth.DefaultSigVerificationGasConsumer, authOptions)
@@ -107,15 +121,31 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) {
func(ctx sdk.Context, tx std.Tx, simulate bool) (
newCtx sdk.Context, res sdk.Result, abort bool,
) {
+ // Add last gas price in the context
+ ctx = ctx.WithValue(auth.GasPriceContextKey{}, gpKpr.LastGasPrice(ctx))
+
// Override auth params.
- ctx = ctx.WithValue(
- auth.AuthParamsContextKey{}, auth.DefaultParams())
+ ctx = ctx.WithValue(auth.AuthParamsContextKey{}, acctKpr.GetParams(ctx))
// Continue on with default auth ante handler.
newCtx, res, abort = authAnteHandler(ctx, tx, simulate)
return
},
)
+ // Set begin and end transaction hooks.
+ // These are used to create gno transaction stores and commit them when finishing
+ // the tx - in other words, data from a failing transaction won't be persisted
+ // to the gno store caches.
+ baseApp.SetBeginTxHook(func(ctx sdk.Context) sdk.Context {
+ // Create Gno transaction store.
+ return vmk.MakeGnoTransactionStore(ctx)
+ })
+ baseApp.SetEndTxHook(func(ctx sdk.Context, result sdk.Result) {
+ if result.IsOK() {
+ vmk.CommitGnoTransactionStore(ctx)
+ }
+ })
+
// Set up the event collector
c := newCollector[validatorUpdate](
cfg.EventSwitch, // global event switch filled by the node
@@ -126,6 +156,8 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) {
baseApp.SetEndBlocker(
EndBlocker(
c,
+ acctKpr,
+ gpKpr,
vmk,
baseApp,
),
@@ -134,6 +166,7 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) {
// Set a handler Route.
baseApp.Router().AddRoute("auth", auth.NewHandler(acctKpr))
baseApp.Router().AddRoute("bank", bank.NewHandler(bankKpr))
+ baseApp.Router().AddRoute("params", params.NewHandler(paramsKpr))
baseApp.Router().AddRoute("vm", vm.NewHandler(vmk))
// Load latest version.
@@ -143,24 +176,49 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) {
// Initialize the VMKeeper.
ms := baseApp.GetCacheMultiStore()
- vmk.Initialize(cfg.Logger, ms, cfg.CacheStdlibLoad)
+ vmk.Initialize(cfg.Logger, ms)
ms.MultiWrite() // XXX why was't this needed?
return baseApp, nil
}
-// NewApp creates the GnoLand application.
+// GenesisAppConfig wraps the most important
+// genesis params relating to the App
+type GenesisAppConfig struct {
+ SkipFailingTxs bool // does not stop the chain from starting if any tx fails
+ SkipSigVerification bool // does not verify the transaction signatures in genesis
+}
+
+// NewTestGenesisAppConfig returns a testing genesis app config
+func NewTestGenesisAppConfig() GenesisAppConfig {
+ return GenesisAppConfig{
+ SkipFailingTxs: true,
+ SkipSigVerification: true,
+ }
+}
+
+// NewApp creates the gno.land application.
func NewApp(
dataRootDir string,
- skipFailingGenesisTxs bool,
+ genesisCfg GenesisAppConfig,
evsw events.EventSwitch,
logger *slog.Logger,
+ minGasPrices string,
) (abci.Application, error) {
var err error
- cfg := NewAppOptions()
- if skipFailingGenesisTxs {
- cfg.GenesisTxHandler = NoopGenesisTxHandler
+ cfg := &AppOptions{
+ Logger: logger,
+ EventSwitch: evsw,
+ InitChainerConfig: InitChainerConfig{
+ GenesisTxResultHandler: PanicOnFailingTxResultHandler,
+ StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"),
+ },
+ MinGasPrices: minGasPrices,
+ SkipGenesisVerification: genesisCfg.SkipSigVerification,
+ }
+ if genesisCfg.SkipFailingTxs {
+ cfg.GenesisTxResultHandler = NoopGenesisTxResultHandler
}
// Get main DB.
@@ -169,74 +227,169 @@ func NewApp(
return nil, fmt.Errorf("error initializing database %q using path %q: %w", dbm.GoLevelDBBackend, dataRootDir, err)
}
- cfg.Logger = logger
- cfg.EventSwitch = evsw
-
return NewAppWithOptions(cfg)
}
-type GenesisTxHandler func(ctx sdk.Context, tx std.Tx, res sdk.Result)
+// GenesisTxResultHandler is called in the InitChainer after a genesis
+// transaction is executed.
+type GenesisTxResultHandler func(ctx sdk.Context, tx std.Tx, res sdk.Result)
-func NoopGenesisTxHandler(_ sdk.Context, _ std.Tx, _ sdk.Result) {}
+// NoopGenesisTxResultHandler is a no-op GenesisTxResultHandler.
+func NoopGenesisTxResultHandler(_ sdk.Context, _ std.Tx, _ sdk.Result) {}
-func PanicOnFailingTxHandler(_ sdk.Context, _ std.Tx, res sdk.Result) {
+// PanicOnFailingTxResultHandler handles genesis transactions by panicking if
+// res.IsErr() returns true.
+func PanicOnFailingTxResultHandler(_ sdk.Context, _ std.Tx, res sdk.Result) {
if res.IsErr() {
panic(res.Log)
}
}
-// InitChainer returns a function that can initialize the chain with genesis.
-func InitChainer(
- baseApp *sdk.BaseApp,
- acctKpr auth.AccountKeeperI,
- bankKpr bank.BankKeeperI,
- resHandler GenesisTxHandler,
-) func(sdk.Context, abci.RequestInitChain) abci.ResponseInitChain {
- return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain {
- txResponses := []abci.ResponseDeliverTx{}
-
- if req.AppState != nil {
- // Get genesis state
- genState := req.AppState.(GnoGenesisState)
-
- // Parse and set genesis state balances
- for _, bal := range genState.Balances {
- acc := acctKpr.NewAccountWithAddress(ctx, bal.Address)
- acctKpr.SetAccount(ctx, acc)
- err := bankKpr.SetCoins(ctx, bal.Address, bal.Amount)
- if err != nil {
- panic(err)
- }
- }
+// InitChainerConfig keeps the configuration for the InitChainer.
+// [NewAppWithOptions] will set [InitChainerConfig.InitChainer] as its InitChainer
+// function.
+type InitChainerConfig struct {
+ // Handles the results of each genesis transaction.
+ GenesisTxResultHandler
+
+ // Standard library directory.
+ StdlibDir string
+ // Whether to keep a record of the DB operations to load standard libraries,
+ // so they can be quickly replicated on additional genesis executions.
+ // This should be used for integration testing, where InitChainer will be
+ // called several times.
+ CacheStdlibLoad bool
- // Run genesis txs
- for _, tx := range genState.Txs {
- res := baseApp.Deliver(tx)
- if res.IsErr() {
- ctx.Logger().Error(
- "Unable to deliver genesis tx",
- "log", res.Log,
- "error", res.Error,
- "gas-used", res.GasUsed,
- )
- }
-
- txResponses = append(txResponses, abci.ResponseDeliverTx{
- ResponseBase: res.ResponseBase,
- GasWanted: res.GasWanted,
- GasUsed: res.GasUsed,
- })
-
- resHandler(ctx, tx, res)
+ // These fields are passed directly by NewAppWithOptions, and should not be
+ // configurable by end-users.
+ baseApp *sdk.BaseApp
+ vmKpr vm.VMKeeperI
+ acctKpr auth.AccountKeeperI
+ bankKpr bank.BankKeeperI
+ paramsKpr params.ParamsKeeperI
+ gpKpr auth.GasPriceKeeperI
+}
+
+// InitChainer is the function that can be used as a [sdk.InitChainer].
+func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain {
+ start := time.Now()
+ ctx.Logger().Debug("InitChainer: started")
+
+ // load standard libraries; immediately committed to store so that they are
+ // available for use when processing the genesis transactions below.
+ cfg.loadStdlibs(ctx)
+ ctx.Logger().Debug("InitChainer: standard libraries loaded",
+ "elapsed", time.Since(start))
+
+ // load app state. AppState may be nil mostly in some minimal testing setups;
+ // so log a warning when that happens.
+ txResponses, err := cfg.loadAppState(ctx, req.AppState)
+ if err != nil {
+ return abci.ResponseInitChain{
+ ResponseBase: abci.ResponseBase{
+ Error: abci.StringError(err.Error()),
+ },
+ }
+ }
+
+ ctx.Logger().Debug("InitChainer: genesis transactions loaded",
+ "elapsed", time.Since(start))
+
+ // Done!
+ return abci.ResponseInitChain{
+ Validators: req.Validators,
+ TxResponses: txResponses,
+ }
+}
+
+func (cfg InitChainerConfig) loadStdlibs(ctx sdk.Context) {
+ // cache-wrapping is necessary for non-validator nodes; in the tm2 BaseApp,
+ // this is done using BaseApp.cacheTxContext; so we replicate it here.
+ ms := ctx.MultiStore()
+ msCache := ms.MultiCacheWrap()
+
+ stdlibCtx := cfg.vmKpr.MakeGnoTransactionStore(ctx)
+ stdlibCtx = stdlibCtx.WithMultiStore(msCache)
+ if cfg.CacheStdlibLoad {
+ cfg.vmKpr.LoadStdlibCached(stdlibCtx, cfg.StdlibDir)
+ } else {
+ cfg.vmKpr.LoadStdlib(stdlibCtx, cfg.StdlibDir)
+ }
+ cfg.vmKpr.CommitGnoTransactionStore(stdlibCtx)
+
+ msCache.MultiWrite()
+}
+
+func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci.ResponseDeliverTx, error) {
+ state, ok := appState.(GnoGenesisState)
+ if !ok {
+ return nil, fmt.Errorf("invalid AppState of type %T", appState)
+ }
+ cfg.acctKpr.InitGenesis(ctx, state.Auth)
+ params := cfg.acctKpr.GetParams(ctx)
+ ctx = ctx.WithValue(auth.AuthParamsContextKey{}, params)
+ auth.InitChainer(ctx, cfg.gpKpr.(auth.GasPriceKeeper), params.InitialGasPrice)
+
+ // Apply genesis balances.
+ for _, bal := range state.Balances {
+ acc := cfg.acctKpr.NewAccountWithAddress(ctx, bal.Address)
+ cfg.acctKpr.SetAccount(ctx, acc)
+ err := cfg.bankKpr.SetCoins(ctx, bal.Address, bal.Amount)
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ // Apply genesis params.
+ for _, param := range state.Params {
+ param.register(ctx, cfg.paramsKpr)
+ }
+
+ // Replay genesis txs.
+ txResponses := make([]abci.ResponseDeliverTx, 0, len(state.Txs))
+
+ // Run genesis txs
+ for _, tx := range state.Txs {
+ var (
+ stdTx = tx.Tx
+ metadata = tx.Metadata
+
+ ctxFn sdk.ContextFn
+ )
+
+ // Check if there is metadata associated with the tx
+ if metadata != nil {
+ // Create a custom context modifier
+ ctxFn = func(ctx sdk.Context) sdk.Context {
+ // Create a copy of the header, in
+ // which only the timestamp information is modified
+ header := ctx.BlockHeader().(*bft.Header).Copy()
+ header.Time = time.Unix(metadata.Timestamp, 0)
+
+ // Save the modified header
+ return ctx.WithBlockHeader(header)
}
}
- // Done!
- return abci.ResponseInitChain{
- Validators: req.Validators,
- TxResponses: txResponses,
+ res := cfg.baseApp.Deliver(stdTx, ctxFn)
+ if res.IsErr() {
+ ctx.Logger().Error(
+ "Unable to deliver genesis tx",
+ "log", res.Log,
+ "error", res.Error,
+ "gas-used", res.GasUsed,
+ )
}
+
+ txResponses = append(txResponses, abci.ResponseDeliverTx{
+ ResponseBase: res.ResponseBase,
+ GasWanted: res.GasWanted,
+ GasUsed: res.GasUsed,
+ })
+
+ cfg.GenesisTxResultHandler(ctx, stdTx, res)
}
+ return txResponses, nil
}
// endBlockerApp is the app abstraction required by any EndBlocker
@@ -253,6 +406,8 @@ type endBlockerApp interface {
// validator set changes
func EndBlocker(
collector *collector[validatorUpdate],
+ acctKpr auth.AccountKeeperI,
+ gpKpr auth.GasPriceKeeperI,
vmk vm.VMKeeperI,
app endBlockerApp,
) func(
@@ -260,6 +415,14 @@ func EndBlocker(
req abci.RequestEndBlock,
) abci.ResponseEndBlock {
return func(ctx sdk.Context, _ abci.RequestEndBlock) abci.ResponseEndBlock {
+ // set the auth params value in the ctx. The EndBlocker will use InitialGasPrice in
+ // the params to calculate the updated gas price.
+ if acctKpr != nil {
+ ctx = ctx.WithValue(auth.AuthParamsContextKey{}, acctKpr.GetParams(ctx))
+ }
+ if acctKpr != nil && gpKpr != nil {
+ auth.EndBlocker(ctx, gpKpr)
+ }
// Check if there was a valset change
if len(collector.getEvents()) == 0 {
// No valset updates
diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go
index 852d090f3af..361d7505157 100644
--- a/gno.land/pkg/gnoland/app_test.go
+++ b/gno.land/pkg/gnoland/app_test.go
@@ -1,20 +1,243 @@
package gnoland
import (
+ "context"
"errors"
"fmt"
"strings"
"testing"
+ "time"
- "github.com/gnolang/gno/gnovm/stdlibs/std"
+ "github.com/gnolang/gno/gno.land/pkg/sdk/vm"
+ "github.com/gnolang/gno/gnovm"
+ gnostdlibs "github.com/gnolang/gno/gnovm/stdlibs/std"
+ "github.com/gnolang/gno/tm2/pkg/amino"
abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
+ bft "github.com/gnolang/gno/tm2/pkg/bft/types"
+ "github.com/gnolang/gno/tm2/pkg/crypto"
+ "github.com/gnolang/gno/tm2/pkg/db/memdb"
"github.com/gnolang/gno/tm2/pkg/events"
+ "github.com/gnolang/gno/tm2/pkg/log"
"github.com/gnolang/gno/tm2/pkg/sdk"
+ "github.com/gnolang/gno/tm2/pkg/sdk/auth"
+ "github.com/gnolang/gno/tm2/pkg/sdk/bank"
+ "github.com/gnolang/gno/tm2/pkg/sdk/params"
+ "github.com/gnolang/gno/tm2/pkg/sdk/testutils"
+ "github.com/gnolang/gno/tm2/pkg/std"
+ "github.com/gnolang/gno/tm2/pkg/store"
+ "github.com/gnolang/gno/tm2/pkg/store/dbadapter"
+ "github.com/gnolang/gno/tm2/pkg/store/iavl"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
+// Tests that NewAppWithOptions works even when only providing a simple DB.
+func TestNewAppWithOptions(t *testing.T) {
+ t.Parallel()
+
+ app, err := NewAppWithOptions(TestAppOptions(memdb.NewMemDB()))
+ require.NoError(t, err)
+ bapp := app.(*sdk.BaseApp)
+ assert.Equal(t, "dev", bapp.AppVersion())
+ assert.Equal(t, "gnoland", bapp.Name())
+
+ addr := crypto.AddressFromPreimage([]byte("test1"))
+
+ appState := DefaultGenState()
+ appState.Balances = []Balance{
+ {
+ Address: addr,
+ Amount: []std.Coin{{Amount: 1e15, Denom: "ugnot"}},
+ },
+ }
+ appState.Txs = []TxWithMetadata{
+ {
+ Tx: std.Tx{
+ Msgs: []std.Msg{vm.NewMsgAddPackage(addr, "gno.land/r/demo", []*gnovm.MemFile{
+ {
+ Name: "demo.gno",
+ Body: "package demo; func Hello() string { return `hello`; }",
+ },
+ })},
+ Fee: std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}},
+ Signatures: []std.Signature{{}}, // one empty signature
+ },
+ },
+ }
+ appState.Params = []Param{
+ {key: "foo", kind: "string", value: "hello"},
+ {key: "foo", kind: "int64", value: int64(-42)},
+ {key: "foo", kind: "uint64", value: uint64(1337)},
+ {key: "foo", kind: "bool", value: true},
+ {key: "foo", kind: "bytes", value: []byte{0x48, 0x69, 0x21}},
+ }
+
+ resp := bapp.InitChain(abci.RequestInitChain{
+ Time: time.Now(),
+ ChainID: "dev",
+ ConsensusParams: &abci.ConsensusParams{
+ Block: defaultBlockParams(),
+ },
+ Validators: []abci.ValidatorUpdate{},
+ AppState: appState,
+ })
+ require.True(t, resp.IsOK(), "InitChain response: %v", resp)
+
+ tx := amino.MustMarshal(std.Tx{
+ Msgs: []std.Msg{vm.NewMsgCall(addr, nil, "gno.land/r/demo", "Hello", nil)},
+ Fee: std.Fee{
+ GasWanted: 100_000,
+ GasFee: std.Coin{
+ Denom: "ugnot",
+ Amount: 1_000_000,
+ },
+ },
+ Signatures: []std.Signature{{}}, // one empty signature
+ Memo: "",
+ })
+ dtxResp := bapp.DeliverTx(abci.RequestDeliverTx{
+ RequestBase: abci.RequestBase{},
+ Tx: tx,
+ })
+ require.True(t, dtxResp.IsOK(), "DeliverTx response: %v", dtxResp)
+
+ cres := bapp.Commit()
+ require.NotNil(t, cres)
+
+ tcs := []struct {
+ path string
+ expectedVal string
+ }{
+ {"params/vm/foo.string", `"hello"`},
+ {"params/vm/foo.int64", `"-42"`},
+ {"params/vm/foo.uint64", `"1337"`},
+ {"params/vm/foo.bool", `true`},
+ {"params/vm/foo.bytes", `"SGkh"`}, // XXX: make this test more readable
+ }
+ for _, tc := range tcs {
+ qres := bapp.Query(abci.RequestQuery{
+ Path: tc.path,
+ })
+ require.True(t, qres.IsOK())
+ assert.Equal(t, qres.Data, []byte(tc.expectedVal))
+ }
+}
+
+func TestNewAppWithOptions_ErrNoDB(t *testing.T) {
+ t.Parallel()
+
+ _, err := NewAppWithOptions(&AppOptions{})
+ assert.ErrorContains(t, err, "no db provided")
+}
+
+func TestNewApp(t *testing.T) {
+ // NewApp should have good defaults and manage to run InitChain.
+ td := t.TempDir()
+
+ app, err := NewApp(td, NewTestGenesisAppConfig(), events.NewEventSwitch(), log.NewNoopLogger(), "")
+ require.NoError(t, err, "NewApp should be successful")
+
+ resp := app.InitChain(abci.RequestInitChain{
+ RequestBase: abci.RequestBase{},
+ Time: time.Time{},
+ ChainID: "dev",
+ ConsensusParams: &abci.ConsensusParams{
+ Block: defaultBlockParams(),
+ Validator: &abci.ValidatorParams{
+ PubKeyTypeURLs: []string{},
+ },
+ },
+ Validators: []abci.ValidatorUpdate{},
+ AppState: DefaultGenState(),
+ })
+ assert.True(t, resp.IsOK(), "resp is not OK: %v", resp)
+}
+
+// Test whether InitChainer calls to load the stdlibs correctly.
+func TestInitChainer_LoadStdlib(t *testing.T) {
+ t.Parallel()
+
+ t.Run("cached", func(t *testing.T) { testInitChainerLoadStdlib(t, true) })
+ t.Run("uncached", func(t *testing.T) { testInitChainerLoadStdlib(t, false) })
+}
+
+func testInitChainerLoadStdlib(t *testing.T, cached bool) { //nolint:thelper
+ t.Parallel()
+
+ type gsContextType string
+ const (
+ stdlibDir = "test-stdlib-dir"
+ gnoStoreKey gsContextType = "gno-store-key"
+ gnoStoreValue gsContextType = "gno-store-value"
+ )
+ db := memdb.NewMemDB()
+ ms := store.NewCommitMultiStore(db)
+ baseCapKey := store.NewStoreKey("baseCapKey")
+ iavlCapKey := store.NewStoreKey("iavlCapKey")
+
+ ms.MountStoreWithDB(baseCapKey, dbadapter.StoreConstructor, db)
+ ms.MountStoreWithDB(iavlCapKey, iavl.StoreConstructor, db)
+ ms.LoadLatestVersion()
+ testCtx := sdk.NewContext(sdk.RunTxModeDeliver, ms.MultiCacheWrap(), &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger())
+
+ // mock set-up
+ var (
+ makeCalls int
+ commitCalls int
+ loadStdlibCalls int
+ loadStdlibCachedCalls int
+ )
+ containsGnoStore := func(ctx sdk.Context) bool {
+ return ctx.Context().Value(gnoStoreKey) == gnoStoreValue
+ }
+ // ptr is pointer to either loadStdlibCalls or loadStdlibCachedCalls
+ loadStdlib := func(ptr *int) func(ctx sdk.Context, dir string) {
+ return func(ctx sdk.Context, dir string) {
+ assert.Equal(t, stdlibDir, dir, "stdlibDir should match provided dir")
+ assert.True(t, containsGnoStore(ctx), "should contain gno store")
+ *ptr++
+ }
+ }
+ mock := &mockVMKeeper{
+ makeGnoTransactionStoreFn: func(ctx sdk.Context) sdk.Context {
+ makeCalls++
+ assert.False(t, containsGnoStore(ctx), "should not already contain gno store")
+ return ctx.WithContext(context.WithValue(ctx.Context(), gnoStoreKey, gnoStoreValue))
+ },
+ commitGnoTransactionStoreFn: func(ctx sdk.Context) {
+ commitCalls++
+ assert.True(t, containsGnoStore(ctx), "should contain gno store")
+ },
+ loadStdlibFn: loadStdlib(&loadStdlibCalls),
+ loadStdlibCachedFn: loadStdlib(&loadStdlibCachedCalls),
+ }
+
+ // call initchainer
+ cfg := InitChainerConfig{
+ StdlibDir: stdlibDir,
+ vmKpr: mock,
+ CacheStdlibLoad: cached,
+ }
+ // Construct keepers.
+ paramsKpr := params.NewParamsKeeper(iavlCapKey, "")
+ cfg.acctKpr = auth.NewAccountKeeper(iavlCapKey, paramsKpr, ProtoGnoAccount)
+ cfg.gpKpr = auth.NewGasPriceKeeper(iavlCapKey)
+ cfg.InitChainer(testCtx, abci.RequestInitChain{
+ AppState: DefaultGenState(),
+ })
+
+ // assert number of calls
+ assert.Equal(t, 1, makeCalls, "should call MakeGnoTransactionStore once")
+ assert.Equal(t, 1, commitCalls, "should call CommitGnoTransactionStore once")
+ if cached {
+ assert.Equal(t, 0, loadStdlibCalls, "should call LoadStdlib never")
+ assert.Equal(t, 1, loadStdlibCachedCalls, "should call LoadStdlibCached once")
+ } else {
+ assert.Equal(t, 1, loadStdlibCalls, "should call LoadStdlib once")
+ assert.Equal(t, 0, loadStdlibCachedCalls, "should call LoadStdlibCached never")
+ }
+}
+
// generateValidatorUpdates generates dummy validator updates
func generateValidatorUpdates(t *testing.T, count int) []abci.ValidatorUpdate {
t.Helper()
@@ -23,7 +246,7 @@ func generateValidatorUpdates(t *testing.T, count int) []abci.ValidatorUpdate {
for i := 0; i < count; i++ {
// Generate a random private key
- key := getDummyKey(t)
+ key := getDummyKey(t).PubKey()
validator := abci.ValidatorUpdate{
Address: key.Address(),
@@ -37,6 +260,189 @@ func generateValidatorUpdates(t *testing.T, count int) []abci.ValidatorUpdate {
return validators
}
+func createAndSignTx(
+ t *testing.T,
+ msgs []std.Msg,
+ chainID string,
+ key crypto.PrivKey,
+) std.Tx {
+ t.Helper()
+
+ tx := std.Tx{
+ Msgs: msgs,
+ Fee: std.Fee{
+ GasFee: std.NewCoin("ugnot", 2000000),
+ GasWanted: 10000000,
+ },
+ }
+
+ signBytes, err := tx.GetSignBytes(chainID, 0, 0)
+ require.NoError(t, err)
+
+ // Sign the tx
+ signedTx, err := key.Sign(signBytes)
+ require.NoError(t, err)
+
+ tx.Signatures = []std.Signature{
+ {
+ PubKey: key.PubKey(),
+ Signature: signedTx,
+ },
+ }
+
+ return tx
+}
+
+func TestInitChainer_MetadataTxs(t *testing.T) {
+ var (
+ currentTimestamp = time.Now()
+ laterTimestamp = currentTimestamp.Add(10 * 24 * time.Hour) // 10 days
+
+ getMetadataState = func(tx std.Tx, balances []Balance) GnoGenesisState {
+ return GnoGenesisState{
+ // Set the package deployment as the genesis tx
+ Txs: []TxWithMetadata{
+ {
+ Tx: tx,
+ Metadata: &GnoTxMetadata{
+ Timestamp: laterTimestamp.Unix(),
+ },
+ },
+ },
+ // Make sure the deployer account has a balance
+ Balances: balances,
+ }
+ }
+
+ getNonMetadataState = func(tx std.Tx, balances []Balance) GnoGenesisState {
+ return GnoGenesisState{
+ Txs: []TxWithMetadata{
+ {
+ Tx: tx,
+ },
+ },
+ Balances: balances,
+ }
+ }
+ )
+
+ testTable := []struct {
+ name string
+ genesisTime time.Time
+ expectedTime time.Time
+ stateFn func(std.Tx, []Balance) GnoGenesisState
+ }{
+ {
+ "non-metadata transaction",
+ currentTimestamp,
+ currentTimestamp,
+ getNonMetadataState,
+ },
+ {
+ "metadata transaction",
+ currentTimestamp,
+ laterTimestamp,
+ getMetadataState,
+ },
+ }
+
+ for _, testCase := range testTable {
+ t.Run(testCase.name, func(t *testing.T) {
+ var (
+ db = memdb.NewMemDB()
+
+ key = getDummyKey(t) // user account, and genesis deployer
+ chainID = "test"
+
+ path = "gno.land/r/demo/metadatatx"
+ body = `package metadatatx
+
+ import "time"
+
+ // Time is initialized on deployment (genesis)
+ var t time.Time = time.Now()
+
+ // GetT returns the time that was saved from genesis
+ func GetT() int64 { return t.Unix() }
+`
+ )
+
+ // Create a fresh app instance
+ app, err := NewAppWithOptions(TestAppOptions(db))
+ require.NoError(t, err)
+
+ // Prepare the deploy transaction
+ msg := vm.MsgAddPackage{
+ Creator: key.PubKey().Address(),
+ Package: &gnovm.MemPackage{
+ Name: "metadatatx",
+ Path: path,
+ Files: []*gnovm.MemFile{
+ {
+ Name: "file.gno",
+ Body: body,
+ },
+ },
+ },
+ Deposit: nil,
+ }
+
+ // Create the initial genesis tx
+ tx := createAndSignTx(t, []std.Msg{msg}, chainID, key)
+
+ // Run the top-level init chain process
+ app.InitChain(abci.RequestInitChain{
+ ChainID: chainID,
+ Time: testCase.genesisTime,
+ ConsensusParams: &abci.ConsensusParams{
+ Block: defaultBlockParams(),
+ Validator: &abci.ValidatorParams{
+ PubKeyTypeURLs: []string{},
+ },
+ },
+ // Set the package deployment as the genesis tx,
+ // and make sure the deployer account has a balance
+ AppState: testCase.stateFn(tx, []Balance{
+ {
+ // Make sure the deployer account has a balance
+ Address: key.PubKey().Address(),
+ Amount: std.NewCoins(std.NewCoin("ugnot", 20_000_000)),
+ },
+ }),
+ })
+
+ // Prepare the call transaction
+ callMsg := vm.MsgCall{
+ Caller: key.PubKey().Address(),
+ PkgPath: path,
+ Func: "GetT",
+ }
+
+ tx = createAndSignTx(t, []std.Msg{callMsg}, chainID, key)
+
+ // Marshal the transaction to Amino binary
+ marshalledTx, err := amino.Marshal(tx)
+ require.NoError(t, err)
+
+ // Execute the call to the "GetT" method
+ // on the deployed Realm
+ resp := app.DeliverTx(abci.RequestDeliverTx{
+ Tx: marshalledTx,
+ })
+
+ require.True(t, resp.IsOK())
+
+ // Make sure the initialized Realm state is
+ // the injected context timestamp from the tx metadata
+ assert.Contains(
+ t,
+ string(resp.Data),
+ fmt.Sprintf("(%d int64)", testCase.expectedTime.Unix()),
+ )
+ })
+ }
+}
+
func TestEndBlocker(t *testing.T) {
t.Parallel()
@@ -81,7 +487,7 @@ func TestEndBlocker(t *testing.T) {
t.Run("no collector events", func(t *testing.T) {
t.Parallel()
- noFilter := func(e events.Event) []validatorUpdate {
+ noFilter := func(_ events.Event) []validatorUpdate {
return []validatorUpdate{}
}
@@ -89,7 +495,7 @@ func TestEndBlocker(t *testing.T) {
c := newCollector[validatorUpdate](&mockEventSwitch{}, noFilter)
// Create the EndBlocker
- eb := EndBlocker(c, nil, &mockEndBlockerApp{})
+ eb := EndBlocker(c, nil, nil, nil, &mockEndBlockerApp{})
// Run the EndBlocker
res := eb(sdk.Context{}, abci.RequestEndBlock{})
@@ -102,7 +508,7 @@ func TestEndBlocker(t *testing.T) {
t.Parallel()
var (
- noFilter = func(e events.Event) []validatorUpdate {
+ noFilter = func(_ events.Event) []validatorUpdate {
return make([]validatorUpdate, 1) // 1 update
}
@@ -126,10 +532,10 @@ func TestEndBlocker(t *testing.T) {
c := newCollector[validatorUpdate](mockEventSwitch, noFilter)
// Fire a GnoVM event
- mockEventSwitch.FireEvent(std.GnoEvent{})
+ mockEventSwitch.FireEvent(gnostdlibs.GnoEvent{})
// Create the EndBlocker
- eb := EndBlocker(c, mockVMKeeper, &mockEndBlockerApp{})
+ eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{})
// Run the EndBlocker
res := eb(sdk.Context{}, abci.RequestEndBlock{})
@@ -145,7 +551,7 @@ func TestEndBlocker(t *testing.T) {
t.Parallel()
var (
- noFilter = func(e events.Event) []validatorUpdate {
+ noFilter = func(_ events.Event) []validatorUpdate {
return make([]validatorUpdate, 1) // 1 update
}
@@ -169,10 +575,10 @@ func TestEndBlocker(t *testing.T) {
c := newCollector[validatorUpdate](mockEventSwitch, noFilter)
// Fire a GnoVM event
- mockEventSwitch.FireEvent(std.GnoEvent{})
+ mockEventSwitch.FireEvent(gnostdlibs.GnoEvent{})
// Create the EndBlocker
- eb := EndBlocker(c, mockVMKeeper, &mockEndBlockerApp{})
+ eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{})
// Run the EndBlocker
res := eb(sdk.Context{}, abci.RequestEndBlock{})
@@ -208,7 +614,7 @@ func TestEndBlocker(t *testing.T) {
// Construct the GnoVM events
vmEvents := make([]abci.Event, 0, len(changes))
for index := range changes {
- event := std.GnoEvent{
+ event := gnostdlibs.GnoEvent{
Type: validatorAddedEvent,
PkgPath: valRealm,
}
@@ -217,7 +623,7 @@ func TestEndBlocker(t *testing.T) {
if index%2 == 0 {
changes[index].Power = 0
- event = std.GnoEvent{
+ event = gnostdlibs.GnoEvent{
Type: validatorRemovedEvent,
PkgPath: valRealm,
}
@@ -227,8 +633,8 @@ func TestEndBlocker(t *testing.T) {
}
// Fire the tx result event
- txEvent := types.EventTx{
- Result: types.TxResult{
+ txEvent := bft.EventTx{
+ Result: bft.TxResult{
Response: abci.ResponseDeliverTx{
ResponseBase: abci.ResponseBase{
Events: vmEvents,
@@ -240,7 +646,7 @@ func TestEndBlocker(t *testing.T) {
mockEventSwitch.FireEvent(txEvent)
// Create the EndBlocker
- eb := EndBlocker(c, mockVMKeeper, &mockEndBlockerApp{})
+ eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{})
// Run the EndBlocker
res := eb(sdk.Context{}, abci.RequestEndBlock{})
@@ -255,3 +661,338 @@ func TestEndBlocker(t *testing.T) {
}
})
}
+
+func TestGasPriceUpdate(t *testing.T) {
+ app := newGasPriceTestApp(t)
+
+ // with default initial gas price 0.1 ugnot per gas
+ gnoGen := gnoGenesisState(t)
+
+ // abci inintChain
+ app.InitChain(abci.RequestInitChain{
+ AppState: gnoGen,
+ ChainID: "test-chain",
+ ConsensusParams: &abci.ConsensusParams{
+ Block: &abci.BlockParams{
+ MaxGas: 10000,
+ },
+ },
+ })
+ baseApp := app.(*sdk.BaseApp)
+ require.Equal(t, int64(0), baseApp.LastBlockHeight())
+ // Case 1
+ // CheckTx failed because the GasFee is less than the initial gas price.
+
+ tx := newCounterTx(100)
+ tx.Fee = std.Fee{
+ GasWanted: 100,
+ GasFee: sdk.Coin{
+ Amount: 9,
+ Denom: "ugnot",
+ },
+ }
+ txBytes, err := amino.Marshal(tx)
+ require.NoError(t, err)
+ r := app.CheckTx(abci.RequestCheckTx{Tx: txBytes})
+ assert.False(t, r.IsOK(), fmt.Sprintf("%v", r))
+
+ // Case 2:
+ // A previously successful CheckTx failed after the block gas price increased.
+ // Check Tx Ok
+ tx2 := newCounterTx(100)
+ tx2.Fee = std.Fee{
+ GasWanted: 1000,
+ GasFee: sdk.Coin{
+ Amount: 100,
+ Denom: "ugnot",
+ },
+ }
+ txBytes2, err := amino.Marshal(tx2)
+ require.NoError(t, err)
+ r = app.CheckTx(abci.RequestCheckTx{Tx: txBytes2})
+ assert.True(t, r.IsOK(), fmt.Sprintf("%v", r))
+
+ // After replaying a block, the gas price increased.
+ header := &bft.Header{ChainID: "test-chain", Height: 1}
+ app.BeginBlock(abci.RequestBeginBlock{Header: header})
+ // Delvier Tx consumes more than that target block gas 6000.
+
+ tx6001 := newCounterTx(6001)
+ tx6001.Fee = std.Fee{
+ GasWanted: 20000,
+ GasFee: sdk.Coin{
+ Amount: 200,
+ Denom: "ugnot",
+ },
+ }
+ txBytes6001, err := amino.Marshal(tx6001)
+ require.NoError(t, err)
+ res := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes6001})
+ require.True(t, res.IsOK(), fmt.Sprintf("%v", res))
+ app.EndBlock(abci.RequestEndBlock{})
+ app.Commit()
+
+ // CheckTx failed because gas price increased
+ r = app.CheckTx(abci.RequestCheckTx{Tx: txBytes2})
+ assert.False(t, r.IsOK(), fmt.Sprintf("%v", r))
+
+ // Case 3:
+ // A previously failed CheckTx successed after block gas price reduced.
+
+ // CheckTx Failed
+ r = app.CheckTx(abci.RequestCheckTx{Tx: txBytes2})
+ assert.False(t, r.IsOK(), fmt.Sprintf("%v", r))
+ // Replayed a Block, the gas price decrease
+ header = &bft.Header{ChainID: "test-chain", Height: 2}
+ app.BeginBlock(abci.RequestBeginBlock{Header: header})
+ // Delvier Tx consumes less than that target block gas 6000.
+
+ tx200 := newCounterTx(200)
+ tx200.Fee = std.Fee{
+ GasWanted: 20000,
+ GasFee: sdk.Coin{
+ Amount: 200,
+ Denom: "ugnot",
+ },
+ }
+ txBytes200, err := amino.Marshal(tx200)
+ require.NoError(t, err)
+
+ res = app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes200})
+ require.True(t, res.IsOK(), fmt.Sprintf("%v", res))
+
+ app.EndBlock(abci.RequestEndBlock{})
+ app.Commit()
+
+ // CheckTx earlier failed tx, now is OK
+ r = app.CheckTx(abci.RequestCheckTx{Tx: txBytes2})
+ assert.True(t, r.IsOK(), fmt.Sprintf("%v", r))
+
+ // Case 4
+ // require matching expected GasPrice after three blocks ( increase case)
+ replayBlock(t, baseApp, 8000, 3)
+ replayBlock(t, baseApp, 8000, 4)
+ replayBlock(t, baseApp, 6000, 5)
+
+ key := []byte("gasPrice")
+ query := abci.RequestQuery{
+ Path: ".store/main/key",
+ Data: key,
+ }
+ qr := app.Query(query)
+ var gp std.GasPrice
+ err = amino.Unmarshal(qr.Value, &gp)
+ require.NoError(t, err)
+ require.Equal(t, "108ugnot", gp.Price.String())
+
+ // Case 5,
+ // require matching expected GasPrice after low gas blocks ( decrease below initial gas price case)
+
+ replayBlock(t, baseApp, 5000, 6)
+ replayBlock(t, baseApp, 5000, 7)
+ replayBlock(t, baseApp, 5000, 8)
+
+ qr = app.Query(query)
+ err = amino.Unmarshal(qr.Value, &gp)
+ require.NoError(t, err)
+ require.Equal(t, "102ugnot", gp.Price.String())
+
+ replayBlock(t, baseApp, 5000, 9)
+
+ qr = app.Query(query)
+ err = amino.Unmarshal(qr.Value, &gp)
+ require.NoError(t, err)
+ require.Equal(t, "100ugnot", gp.Price.String())
+}
+
+func newGasPriceTestApp(t *testing.T) abci.Application {
+ t.Helper()
+ cfg := TestAppOptions(memdb.NewMemDB())
+ cfg.EventSwitch = events.NewEventSwitch()
+
+ // Capabilities keys.
+ mainKey := store.NewStoreKey("main")
+ baseKey := store.NewStoreKey("base")
+
+ baseApp := sdk.NewBaseApp("gnoland", cfg.Logger, cfg.DB, baseKey, mainKey)
+ baseApp.SetAppVersion("test")
+
+ // Set mounts for BaseApp's MultiStore.
+ baseApp.MountStoreWithDB(mainKey, iavl.StoreConstructor, cfg.DB)
+ baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB)
+
+ // Construct keepers.
+ paramsKpr := params.NewParamsKeeper(mainKey, "")
+ acctKpr := auth.NewAccountKeeper(mainKey, paramsKpr, ProtoGnoAccount)
+ gpKpr := auth.NewGasPriceKeeper(mainKey)
+ bankKpr := bank.NewBankKeeper(acctKpr)
+ vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, paramsKpr)
+
+ // Set InitChainer
+ icc := cfg.InitChainerConfig
+ icc.baseApp = baseApp
+ icc.acctKpr, icc.bankKpr, icc.vmKpr, icc.gpKpr = acctKpr, bankKpr, vmk, gpKpr
+ baseApp.SetInitChainer(icc.InitChainer)
+
+ // Set AnteHandler
+ baseApp.SetAnteHandler(
+ // Override default AnteHandler with custom logic.
+ func(ctx sdk.Context, tx std.Tx, simulate bool) (
+ newCtx sdk.Context, res sdk.Result, abort bool,
+ ) {
+ // Add last gas price in the context
+ ctx = ctx.WithValue(auth.GasPriceContextKey{}, gpKpr.LastGasPrice(ctx))
+
+ // Override auth params.
+ ctx = ctx.WithValue(auth.AuthParamsContextKey{}, acctKpr.GetParams(ctx))
+ // Continue on with default auth ante handler.
+ if ctx.IsCheckTx() {
+ res := auth.EnsureSufficientMempoolFees(ctx, tx.Fee)
+ if !res.IsOK() {
+ return ctx, res, true
+ }
+ }
+
+ newCtx = auth.SetGasMeter(false, ctx, tx.Fee.GasWanted)
+
+ count := getTotalCount(tx)
+
+ newCtx.GasMeter().ConsumeGas(count, "counter-ante")
+ res = sdk.Result{
+ GasWanted: getTotalCount(tx),
+ }
+ return
+ },
+ )
+
+ // Set up the event collector
+ c := newCollector[validatorUpdate](
+ cfg.EventSwitch, // global event switch filled by the node
+ validatorEventFilter, // filter fn that keeps the collector valid
+ )
+
+ // Set EndBlocker
+ baseApp.SetEndBlocker(
+ EndBlocker(
+ c,
+ acctKpr,
+ gpKpr,
+ nil,
+ baseApp,
+ ),
+ )
+
+ // Set a handler Route.
+ baseApp.Router().AddRoute("auth", auth.NewHandler(acctKpr))
+ baseApp.Router().AddRoute("bank", bank.NewHandler(bankKpr))
+ baseApp.Router().AddRoute(
+ testutils.RouteMsgCounter,
+ newTestHandler(
+ func(ctx sdk.Context, msg sdk.Msg) sdk.Result { return sdk.Result{} },
+ ),
+ )
+
+ baseApp.Router().AddRoute("vm", vm.NewHandler(vmk))
+
+ // Load latest version.
+ if err := baseApp.LoadLatestVersion(); err != nil {
+ t.Fatalf("failed to load the lastest state: %v", err)
+ }
+
+ // Initialize the VMKeeper.
+ ms := baseApp.GetCacheMultiStore()
+ vmk.Initialize(cfg.Logger, ms)
+ ms.MultiWrite() // XXX why was't this needed?
+
+ return baseApp
+}
+
+// newTx constructs a tx with multiple counter messages.
+// we can use the counter as the gas used for the message.
+
+func newCounterTx(counters ...int64) sdk.Tx {
+ msgs := make([]sdk.Msg, len(counters))
+
+ for i, c := range counters {
+ msgs[i] = testutils.MsgCounter{Counter: c}
+ }
+ tx := sdk.Tx{Msgs: msgs}
+ return tx
+}
+
+func getTotalCount(tx sdk.Tx) int64 {
+ var c int64
+ for _, m := range tx.Msgs {
+ c = +m.(testutils.MsgCounter).Counter
+ }
+ return c
+}
+
+func gnoGenesisState(t *testing.T) GnoGenesisState {
+ t.Helper()
+ gen := GnoGenesisState{}
+ genBytes := []byte(`{
+ "@type": "/gno.GenesisState",
+ "auth": {
+ "params": {
+ "gas_price_change_compressor": "8",
+ "initial_gasprice": {
+ "gas": "1000",
+ "price": "100ugnot"
+ },
+ "max_memo_bytes": "65536",
+ "sig_verify_cost_ed25519": "590",
+ "sig_verify_cost_secp256k1": "1000",
+ "target_gas_ratio": "60",
+ "tx_sig_limit": "7",
+ "tx_size_cost_per_byte": "10"
+ }
+ }
+ }`)
+ err := amino.UnmarshalJSON(genBytes, &gen)
+ if err != nil {
+ t.Fatalf("failed to create genesis state: %v", err)
+ }
+ return gen
+}
+
+func replayBlock(t *testing.T, app *sdk.BaseApp, gas int64, hight int64) {
+ t.Helper()
+ tx := newCounterTx(gas)
+ tx.Fee = std.Fee{
+ GasWanted: 20000,
+ GasFee: sdk.Coin{
+ Amount: 1000,
+ Denom: "ugnot",
+ },
+ }
+ txBytes, err := amino.Marshal(tx)
+ require.NoError(t, err)
+
+ header := &bft.Header{ChainID: "test-chain", Height: hight}
+ app.BeginBlock(abci.RequestBeginBlock{Header: header})
+ // consume gas in the block
+ res := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes})
+ require.True(t, res.IsOK(), fmt.Sprintf("%v", res))
+ app.EndBlock(abci.RequestEndBlock{})
+ app.Commit()
+}
+
+type testHandler struct {
+ process func(sdk.Context, sdk.Msg) sdk.Result
+ query func(sdk.Context, abci.RequestQuery) abci.ResponseQuery
+}
+
+func (th testHandler) Process(ctx sdk.Context, msg sdk.Msg) sdk.Result {
+ return th.process(ctx, msg)
+}
+
+func (th testHandler) Query(ctx sdk.Context, req abci.RequestQuery) abci.ResponseQuery {
+ return th.query(ctx, req)
+}
+
+func newTestHandler(proc func(sdk.Context, sdk.Msg) sdk.Result) sdk.Handler {
+ return testHandler{
+ process: proc,
+ }
+}
diff --git a/gno.land/pkg/gnoland/balance_test.go b/gno.land/pkg/gnoland/balance_test.go
index 99a348e9f2f..489384196ad 100644
--- a/gno.land/pkg/gnoland/balance_test.go
+++ b/gno.land/pkg/gnoland/balance_test.go
@@ -120,7 +120,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) {
for index, key := range dummyKeys {
entries[index] = fmt.Sprintf(
"%s=%s",
- key.Address().String(),
+ key.PubKey().Address().String(),
ugnot.ValueString(amount.AmountOf(ugnot.Denom)),
)
}
@@ -131,7 +131,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) {
// Validate the balance map
assert.Len(t, balanceMap, len(dummyKeys))
for _, key := range dummyKeys {
- assert.Equal(t, amount, balanceMap[key.Address()].Amount)
+ assert.Equal(t, amount, balanceMap[key.PubKey().Address()].Amount)
}
})
@@ -162,7 +162,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) {
t.Run("malformed balance, invalid amount", func(t *testing.T) {
t.Parallel()
- dummyKey := getDummyKey(t)
+ dummyKey := getDummyKey(t).PubKey()
balances := []string{
fmt.Sprintf(
@@ -194,7 +194,7 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) {
for index, key := range dummyKeys {
balances[index] = fmt.Sprintf(
"%s=%s",
- key.Address().String(),
+ key.PubKey().Address().String(),
ugnot.ValueString(amount.AmountOf(ugnot.Denom)),
)
}
@@ -206,14 +206,14 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) {
// Validate the balance map
assert.Len(t, balanceMap, len(dummyKeys))
for _, key := range dummyKeys {
- assert.Equal(t, amount, balanceMap[key.Address()].Amount)
+ assert.Equal(t, amount, balanceMap[key.PubKey().Address()].Amount)
}
})
t.Run("malformed balance, invalid amount", func(t *testing.T) {
t.Parallel()
- dummyKey := getDummyKey(t)
+ dummyKey := getDummyKey(t).PubKey()
balances := []string{
fmt.Sprintf(
@@ -236,9 +236,8 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) {
// XXX: this function should probably be exposed somewhere as it's duplicate of
// cmd/genesis/...
-// getDummyKey generates a random public key,
-// and returns the key info
-func getDummyKey(t *testing.T) crypto.PubKey {
+// getDummyKey generates a random private key
+func getDummyKey(t *testing.T) crypto.PrivKey {
t.Helper()
mnemonic, err := client.GenerateMnemonic(256)
@@ -246,14 +245,14 @@ func getDummyKey(t *testing.T) crypto.PubKey {
seed := bip39.NewSeed(mnemonic, "")
- return generateKeyFromSeed(seed, 0).PubKey()
+ return generateKeyFromSeed(seed, 0)
}
// getDummyKeys generates random keys for testing
-func getDummyKeys(t *testing.T, count int) []crypto.PubKey {
+func getDummyKeys(t *testing.T, count int) []crypto.PrivKey {
t.Helper()
- dummyKeys := make([]crypto.PubKey, count)
+ dummyKeys := make([]crypto.PrivKey, count)
for i := 0; i < count; i++ {
dummyKeys[i] = getDummyKey(t)
diff --git a/gno.land/pkg/gnoland/genesis.go b/gno.land/pkg/gnoland/genesis.go
index f5f0aa56758..a754e7a4644 100644
--- a/gno.land/pkg/gnoland/genesis.go
+++ b/gno.land/pkg/gnoland/genesis.go
@@ -12,16 +12,23 @@ import (
bft "github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/gnolang/gno/tm2/pkg/crypto"
osm "github.com/gnolang/gno/tm2/pkg/os"
+ "github.com/gnolang/gno/tm2/pkg/sdk/auth"
"github.com/gnolang/gno/tm2/pkg/std"
+ "github.com/pelletier/go-toml"
)
+const initGasPrice = "1ugnot/1000gas"
+
// LoadGenesisBalancesFile loads genesis balances from the provided file path.
-func LoadGenesisBalancesFile(path string) ([]Balance, error) {
+func LoadGenesisBalancesFile(path string) (Balances, error) {
// each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot
- content := osm.MustReadFile(path)
+ content, err := osm.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
lines := strings.Split(string(content), "\n")
- balances := make([]Balance, 0, len(lines))
+ balances := make(Balances, len(lines))
for _, line := range lines {
line = strings.TrimSpace(line)
@@ -49,20 +56,60 @@ func LoadGenesisBalancesFile(path string) ([]Balance, error) {
return nil, fmt.Errorf("invalid balance coins %s: %w", parts[1], err)
}
- balances = append(balances, Balance{
- Address: addr,
- Amount: coins,
- })
+ balances.Set(addr, coins)
}
return balances, nil
}
+// LoadGenesisParamsFile loads genesis params from the provided file path.
+func LoadGenesisParamsFile(path string) ([]Param, error) {
+ // each param is in the form: key.kind=value
+ content, err := osm.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+
+ m := map[string] /*category*/ map[string] /*key*/ map[string] /*kind*/ interface{} /*value*/ {}
+ err = toml.Unmarshal(content, &m)
+ if err != nil {
+ return nil, err
+ }
+
+ params := make([]Param, 0)
+ for category, keys := range m {
+ for key, kinds := range keys {
+ for kind, val := range kinds {
+ param := Param{
+ key: category + "." + key,
+ kind: kind,
+ }
+ switch kind {
+ case "uint64": // toml
+ param.value = uint64(val.(int64))
+ default:
+ param.value = val
+ }
+ if err := param.Verify(); err != nil {
+ return nil, err
+ }
+ params = append(params, param)
+ }
+ }
+ }
+
+ return params, nil
+}
+
// LoadGenesisTxsFile loads genesis transactions from the provided file path.
// XXX: Improve the way we generate and load this file
-func LoadGenesisTxsFile(path string, chainID string, genesisRemote string) ([]std.Tx, error) {
- txs := []std.Tx{}
- txsBz := osm.MustReadFile(path)
+func LoadGenesisTxsFile(path string, chainID string, genesisRemote string) ([]TxWithMetadata, error) {
+ txs := make([]TxWithMetadata, 0)
+
+ txsBz, err := osm.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
txsLines := strings.Split(string(txsBz), "\n")
for _, txLine := range txsLines {
if txLine == "" {
@@ -73,7 +120,7 @@ func LoadGenesisTxsFile(path string, chainID string, genesisRemote string) ([]st
txLine = strings.ReplaceAll(txLine, "%%CHAINID%%", chainID)
txLine = strings.ReplaceAll(txLine, "%%REMOTE%%", genesisRemote)
- var tx std.Tx
+ var tx TxWithMetadata
if err := amino.UnmarshalJSON([]byte(txLine), &tx); err != nil {
return nil, fmt.Errorf("unable to Unmarshall txs file: %w", err)
}
@@ -86,7 +133,7 @@ func LoadGenesisTxsFile(path string, chainID string, genesisRemote string) ([]st
// LoadPackagesFromDir loads gno packages from a directory.
// It creates and returns a list of transactions based on these packages.
-func LoadPackagesFromDir(dir string, creator bft.Address, fee std.Fee) ([]std.Tx, error) {
+func LoadPackagesFromDir(dir string, creator bft.Address, fee std.Fee) ([]TxWithMetadata, error) {
// list all packages from target path
pkgs, err := gnomod.ListPkgs(dir)
if err != nil {
@@ -101,14 +148,16 @@ func LoadPackagesFromDir(dir string, creator bft.Address, fee std.Fee) ([]std.Tx
// Filter out draft packages.
nonDraftPkgs := sortedPkgs.GetNonDraftPkgs()
- txs := []std.Tx{}
+ txs := make([]TxWithMetadata, 0, len(nonDraftPkgs))
for _, pkg := range nonDraftPkgs {
tx, err := LoadPackage(pkg, creator, fee, nil)
if err != nil {
return nil, fmt.Errorf("unable to load package %q: %w", pkg.Dir, err)
}
- txs = append(txs, tx)
+ txs = append(txs, TxWithMetadata{
+ Tx: tx,
+ })
}
return txs, nil
@@ -119,7 +168,7 @@ func LoadPackage(pkg gnomod.Pkg, creator bft.Address, fee std.Fee, deposit std.C
var tx std.Tx
// Open files in directory as MemPackage.
- memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name)
+ memPkg := gno.MustReadMemPackage(pkg.Dir, pkg.Name)
err := memPkg.Validate()
if err != nil {
return tx, fmt.Errorf("invalid package: %w", err)
@@ -138,3 +187,20 @@ func LoadPackage(pkg gnomod.Pkg, creator bft.Address, fee std.Fee, deposit std.C
return tx, nil
}
+
+func DefaultGenState() GnoGenesisState {
+ authGen := auth.DefaultGenesisState()
+ gp, err := std.ParseGasPrice(initGasPrice)
+ if err != nil {
+ panic(err)
+ }
+ authGen.Params.InitialGasPrice = gp
+
+ gs := GnoGenesisState{
+ Balances: []Balance{},
+ Txs: []TxWithMetadata{},
+ Auth: authGen,
+ }
+
+ return gs
+}
diff --git a/gno.land/pkg/gnoland/mock_test.go b/gno.land/pkg/gnoland/mock_test.go
index 1ff9f168bd1..62aecaf5278 100644
--- a/gno.land/pkg/gnoland/mock_test.go
+++ b/gno.land/pkg/gnoland/mock_test.go
@@ -45,18 +45,15 @@ func (m *mockEventSwitch) RemoveListener(listenerID string) {
}
}
-type (
- addPackageDelegate func(sdk.Context, vm.MsgAddPackage) error
- callDelegate func(sdk.Context, vm.MsgCall) (string, error)
- queryEvalDelegate func(sdk.Context, string, string) (string, error)
- runDelegate func(sdk.Context, vm.MsgRun) (string, error)
-)
-
type mockVMKeeper struct {
- addPackageFn addPackageDelegate
- callFn callDelegate
- queryFn queryEvalDelegate
- runFn runDelegate
+ addPackageFn func(sdk.Context, vm.MsgAddPackage) error
+ callFn func(sdk.Context, vm.MsgCall) (string, error)
+ queryFn func(sdk.Context, string, string) (string, error)
+ runFn func(sdk.Context, vm.MsgRun) (string, error)
+ loadStdlibFn func(sdk.Context, string)
+ loadStdlibCachedFn func(sdk.Context, string)
+ makeGnoTransactionStoreFn func(ctx sdk.Context) sdk.Context
+ commitGnoTransactionStoreFn func(ctx sdk.Context)
}
func (m *mockVMKeeper) AddPackage(ctx sdk.Context, msg vm.MsgAddPackage) error {
@@ -91,6 +88,31 @@ func (m *mockVMKeeper) Run(ctx sdk.Context, msg vm.MsgRun) (res string, err erro
return "", nil
}
+func (m *mockVMKeeper) LoadStdlib(ctx sdk.Context, stdlibDir string) {
+ if m.loadStdlibFn != nil {
+ m.loadStdlibFn(ctx, stdlibDir)
+ }
+}
+
+func (m *mockVMKeeper) LoadStdlibCached(ctx sdk.Context, stdlibDir string) {
+ if m.loadStdlibCachedFn != nil {
+ m.loadStdlibCachedFn(ctx, stdlibDir)
+ }
+}
+
+func (m *mockVMKeeper) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context {
+ if m.makeGnoTransactionStoreFn != nil {
+ return m.makeGnoTransactionStoreFn(ctx)
+ }
+ return ctx
+}
+
+func (m *mockVMKeeper) CommitGnoTransactionStore(ctx sdk.Context) {
+ if m.commitGnoTransactionStoreFn != nil {
+ m.commitGnoTransactionStoreFn(ctx)
+ }
+}
+
type (
lastBlockHeightDelegate func() int64
loggerDelegate func() *slog.Logger
diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go
index 02691f89c3e..cc9e74a78d8 100644
--- a/gno.land/pkg/gnoland/node_inmemory.go
+++ b/gno.land/pkg/gnoland/node_inmemory.go
@@ -2,7 +2,9 @@ package gnoland
import (
"fmt"
+ "io"
"log/slog"
+ "path/filepath"
"time"
abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
@@ -14,16 +16,19 @@ import (
"github.com/gnolang/gno/tm2/pkg/db"
"github.com/gnolang/gno/tm2/pkg/db/memdb"
"github.com/gnolang/gno/tm2/pkg/events"
- "github.com/gnolang/gno/tm2/pkg/p2p"
- "github.com/gnolang/gno/tm2/pkg/std"
+ "github.com/gnolang/gno/tm2/pkg/p2p/types"
)
type InMemoryNodeConfig struct {
- PrivValidator bft.PrivValidator // identity of the validator
- Genesis *bft.GenesisDoc
- TMConfig *tmcfg.Config
- GenesisTxHandler GenesisTxHandler
- GenesisMaxVMCycles int64
+ PrivValidator bft.PrivValidator // identity of the validator
+ Genesis *bft.GenesisDoc
+ TMConfig *tmcfg.Config
+ DB db.DB // will be initialized if nil
+ VMOutput io.Writer // optional
+ SkipGenesisVerification bool
+
+ // If StdlibDir not set, then it's filepath.Join(TMConfig.RootDir, "gnovm", "stdlibs")
+ InitChainerConfig
}
// NewMockedPrivValidator generate a new key
@@ -32,25 +37,36 @@ func NewMockedPrivValidator() bft.PrivValidator {
}
// NewDefaultGenesisConfig creates a default configuration for an in-memory node.
-func NewDefaultGenesisConfig(chainid string) *bft.GenesisDoc {
+func NewDefaultGenesisConfig(chainid, chaindomain string) *bft.GenesisDoc {
+ // custom chain domain
+ var domainParam Param
+ _ = domainParam.Parse("gno.land/r/sys/params.vm.chain_domain.string=" + chaindomain)
+
return &bft.GenesisDoc{
GenesisTime: time.Now(),
ChainID: chainid,
ConsensusParams: abci.ConsensusParams{
- Block: &abci.BlockParams{
- MaxTxBytes: 1_000_000, // 1MB,
- MaxDataBytes: 2_000_000, // 2MB,
- MaxGas: 100_000_000, // 100M gas
- TimeIotaMS: 100, // 100ms
- },
+ Block: defaultBlockParams(),
},
AppState: &GnoGenesisState{
Balances: []Balance{},
- Txs: []std.Tx{},
+ Txs: []TxWithMetadata{},
+ Params: []Param{
+ domainParam,
+ },
},
}
}
+func defaultBlockParams() *abci.BlockParams {
+ return &abci.BlockParams{
+ MaxTxBytes: 1_000_000, // 1MB,
+ MaxDataBytes: 2_000_000, // 2MB,
+ MaxGas: 100_000_000, // 100M gas
+ TimeIotaMS: 100, // 100ms
+ }
+}
+
func NewDefaultTMConfig(rootdir string) *tmcfg.Config {
// We use `TestConfig` here otherwise ChainID will be empty, and
// there is no other way to update it than using a config file
@@ -70,7 +86,7 @@ func (cfg *InMemoryNodeConfig) validate() error {
return fmt.Errorf("`TMConfig.RootDir` is required to locate `stdlibs` directory")
}
- if cfg.GenesisTxHandler == nil {
+ if cfg.GenesisTxResultHandler == nil {
return fmt.Errorf("`GenesisTxHandler` is required but not provided")
}
@@ -87,15 +103,22 @@ func NewInMemoryNode(logger *slog.Logger, cfg *InMemoryNodeConfig) (*node.Node,
evsw := events.NewEventSwitch()
+ if cfg.StdlibDir == "" {
+ cfg.StdlibDir = filepath.Join(cfg.TMConfig.RootDir, "gnovm", "stdlibs")
+ }
+ // initialize db if nil
+ if cfg.DB == nil {
+ cfg.DB = memdb.NewMemDB()
+ }
+
// Initialize the application with the provided options
gnoApp, err := NewAppWithOptions(&AppOptions{
- Logger: logger,
- GnoRootDir: cfg.TMConfig.RootDir,
- GenesisTxHandler: cfg.GenesisTxHandler,
- MaxCycles: cfg.GenesisMaxVMCycles,
- DB: memdb.NewMemDB(),
- EventSwitch: evsw,
- CacheStdlibLoad: true,
+ Logger: logger,
+ DB: cfg.DB,
+ EventSwitch: evsw,
+ InitChainerConfig: cfg.InitChainerConfig,
+ VMOutput: cfg.VMOutput,
+ SkipGenesisVerification: cfg.SkipGenesisVerification,
})
if err != nil {
return nil, fmt.Errorf("error initializing new app: %w", err)
@@ -114,10 +137,10 @@ func NewInMemoryNode(logger *slog.Logger, cfg *InMemoryNodeConfig) (*node.Node,
// Create genesis factory
genProvider := func() (*bft.GenesisDoc, error) { return cfg.Genesis, nil }
- dbProvider := func(*node.DBContext) (db.DB, error) { return memdb.NewMemDB(), nil }
+ dbProvider := func(*node.DBContext) (db.DB, error) { return cfg.DB, nil }
// Generate p2p node identity
- nodekey := &p2p.NodeKey{PrivKey: ed25519.GenPrivKey()}
+ nodekey := &types.NodeKey{PrivKey: ed25519.GenPrivKey()}
// Create and return the in-memory node instance
return node.NewNode(cfg.TMConfig,
diff --git a/gno.land/pkg/gnoland/package.go b/gno.land/pkg/gnoland/package.go
index fd1afbde136..e4b2449c972 100644
--- a/gno.land/pkg/gnoland/package.go
+++ b/gno.land/pkg/gnoland/package.go
@@ -11,4 +11,6 @@ var Package = amino.RegisterPackage(amino.NewPackage(
).WithDependencies().WithTypes(
&GnoAccount{}, "Account",
GnoGenesisState{}, "GenesisState",
+ TxWithMetadata{}, "TxWithMetadata",
+ GnoTxMetadata{}, "GnoTxMetadata",
))
diff --git a/gno.land/pkg/gnoland/param.go b/gno.land/pkg/gnoland/param.go
new file mode 100644
index 00000000000..4c1e1190751
--- /dev/null
+++ b/gno.land/pkg/gnoland/param.go
@@ -0,0 +1,121 @@
+package gnoland
+
+import (
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/gnolang/gno/tm2/pkg/sdk"
+ "github.com/gnolang/gno/tm2/pkg/sdk/params"
+)
+
+type Param struct {
+ key string
+ kind string
+ value interface{}
+}
+
+func (p Param) Verify() error {
+ // XXX: validate
+ return nil
+}
+
+const (
+ ParamKindString = "string"
+ ParamKindInt64 = "int64"
+ ParamKindUint64 = "uint64"
+ ParamKindBool = "bool"
+ ParamKindBytes = "bytes"
+)
+
+func (p *Param) Parse(entry string) error {
+ parts := strings.SplitN(strings.TrimSpace(entry), "=", 2) // .=
+ if len(parts) != 2 {
+ return fmt.Errorf("malformed entry: %q", entry)
+ }
+
+ keyWithKind := parts[0]
+ rawValue := parts[1]
+ p.kind = keyWithKind[strings.LastIndex(keyWithKind, ".")+1:]
+ p.key = strings.TrimSuffix(keyWithKind, "."+p.kind)
+ switch p.kind {
+ case ParamKindString:
+ p.value = rawValue
+ case ParamKindInt64:
+ v, err := strconv.ParseInt(rawValue, 10, 64)
+ if err != nil {
+ return err
+ }
+ p.value = v
+ case ParamKindBool:
+ v, err := strconv.ParseBool(rawValue)
+ if err != nil {
+ return err
+ }
+ p.value = v
+ case ParamKindUint64:
+ v, err := strconv.ParseUint(rawValue, 10, 64)
+ if err != nil {
+ return err
+ }
+ p.value = v
+ case ParamKindBytes:
+ v, err := hex.DecodeString(rawValue)
+ if err != nil {
+ return err
+ }
+ p.value = v
+ default:
+ return errors.New("unsupported param kind: " + p.kind + " (" + entry + ")")
+ }
+
+ return p.Verify()
+}
+
+func (p Param) String() string {
+ typedKey := p.key + "." + p.kind
+ switch p.kind {
+ case ParamKindString:
+ return fmt.Sprintf("%s=%s", typedKey, p.value)
+ case ParamKindInt64:
+ return fmt.Sprintf("%s=%d", typedKey, p.value)
+ case ParamKindUint64:
+ return fmt.Sprintf("%s=%d", typedKey, p.value)
+ case ParamKindBool:
+ if p.value.(bool) {
+ return fmt.Sprintf("%s=true", typedKey)
+ }
+ return fmt.Sprintf("%s=false", typedKey)
+ case ParamKindBytes:
+ return fmt.Sprintf("%s=%x", typedKey, p.value)
+ }
+ panic("invalid param kind:" + p.kind)
+}
+
+func (p *Param) UnmarshalAmino(rep string) error {
+ return p.Parse(rep)
+}
+
+func (p Param) MarshalAmino() (string, error) {
+ return p.String(), nil
+}
+
+func (p Param) register(ctx sdk.Context, prk params.ParamsKeeperI) {
+ key := p.key + "." + p.kind
+ switch p.kind {
+ case ParamKindString:
+ prk.SetString(ctx, key, p.value.(string))
+ case ParamKindInt64:
+ prk.SetInt64(ctx, key, p.value.(int64))
+ case ParamKindUint64:
+ prk.SetUint64(ctx, key, p.value.(uint64))
+ case ParamKindBool:
+ prk.SetBool(ctx, key, p.value.(bool))
+ case ParamKindBytes:
+ prk.SetBytes(ctx, key, p.value.([]byte))
+ default:
+ panic("invalid param kind: " + p.kind)
+ }
+}
diff --git a/gno.land/pkg/gnoland/param_test.go b/gno.land/pkg/gnoland/param_test.go
new file mode 100644
index 00000000000..5d17aab40da
--- /dev/null
+++ b/gno.land/pkg/gnoland/param_test.go
@@ -0,0 +1,41 @@
+package gnoland
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParam_Parse(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ name string
+ entry string
+ expected Param
+ expectErr bool
+ }{
+ {"valid string", "foo.string=hello", Param{key: "foo", kind: "string", value: "hello"}, false},
+ {"valid int64", "foo.int64=-1337", Param{key: "foo", kind: "int64", value: int64(-1337)}, false},
+ {"valid uint64", "foo.uint64=42", Param{key: "foo", kind: "uint64", value: uint64(42)}, false},
+ {"valid bool", "foo.bool=true", Param{key: "foo", kind: "bool", value: true}, false},
+ {"valid bytes", "foo.bytes=AAAA", Param{key: "foo", kind: "bytes", value: []byte{0xaa, 0xaa}}, false},
+ {"invalid key", "invalidkey=foo", Param{}, true},
+ {"invalid kind", "invalid.kind=foo", Param{}, true},
+ {"invalid int64", "invalid.int64=foobar", Param{}, true},
+ {"invalid uint64", "invalid.uint64=-42", Param{}, true},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ param := Param{}
+ err := param.Parse(tc.entry)
+ if tc.expectErr {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, tc.expected, param)
+ }
+ })
+ }
+}
diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go
index 016f3279dbd..66fb2f54e8a 100644
--- a/gno.land/pkg/gnoland/types.go
+++ b/gno.land/pkg/gnoland/types.go
@@ -1,8 +1,15 @@
package gnoland
import (
+ "bufio"
+ "context"
"errors"
+ "fmt"
+ "os"
+ "github.com/gnolang/gno/tm2/pkg/amino"
+ "github.com/gnolang/gno/tm2/pkg/crypto"
+ "github.com/gnolang/gno/tm2/pkg/sdk/auth"
"github.com/gnolang/gno/tm2/pkg/std"
)
@@ -20,6 +27,91 @@ func ProtoGnoAccount() std.Account {
}
type GnoGenesisState struct {
- Balances []Balance `json:"balances"`
- Txs []std.Tx `json:"txs"`
+ Balances []Balance `json:"balances"`
+ Txs []TxWithMetadata `json:"txs"`
+ Params []Param `json:"params"`
+ Auth auth.GenesisState `json:"auth"`
+}
+
+type TxWithMetadata struct {
+ Tx std.Tx `json:"tx"`
+ Metadata *GnoTxMetadata `json:"metadata,omitempty"`
+}
+
+type GnoTxMetadata struct {
+ Timestamp int64 `json:"timestamp"`
+}
+
+// ReadGenesisTxs reads the genesis txs from the given file path
+func ReadGenesisTxs(ctx context.Context, path string) ([]TxWithMetadata, error) {
+ // Open the txs file
+ file, loadErr := os.Open(path)
+ if loadErr != nil {
+ return nil, fmt.Errorf("unable to open tx file %s: %w", path, loadErr)
+ }
+ defer file.Close()
+
+ var (
+ txs []TxWithMetadata
+
+ scanner = bufio.NewScanner(file)
+ )
+
+ scanner.Buffer(make([]byte, 1_000_000), 2_000_000)
+
+ for scanner.Scan() {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ default:
+ // Parse the amino JSON
+ var tx TxWithMetadata
+ if err := amino.UnmarshalJSON(scanner.Bytes(), &tx); err != nil {
+ return nil, fmt.Errorf(
+ "unable to unmarshal amino JSON, %w",
+ err,
+ )
+ }
+
+ txs = append(txs, tx)
+ }
+ }
+
+ // Check for scanning errors
+ if err := scanner.Err(); err != nil {
+ return nil, fmt.Errorf(
+ "error encountered while reading file, %w",
+ err,
+ )
+ }
+
+ return txs, nil
+}
+
+// SignGenesisTxs will sign all txs passed as argument using the private key.
+// This signature is only valid for genesis transactions as the account number and sequence are 0
+func SignGenesisTxs(txs []TxWithMetadata, privKey crypto.PrivKey, chainID string) error {
+ for index, tx := range txs {
+ // Upon verifying genesis transactions, the account number and sequence are considered to be 0.
+ // The reason for this is that it is not possible to know the account number (or sequence!) in advance
+ // when generating the genesis transaction signature
+ bytes, err := tx.Tx.GetSignBytes(chainID, 0, 0)
+ if err != nil {
+ return fmt.Errorf("unable to get sign bytes for transaction, %w", err)
+ }
+
+ signature, err := privKey.Sign(bytes)
+ if err != nil {
+ return fmt.Errorf("unable to sign genesis transaction, %w", err)
+ }
+
+ txs[index].Tx.Signatures = []std.Signature{
+ {
+ PubKey: privKey.PubKey(),
+ Signature: signature,
+ },
+ }
+ }
+
+ return nil
}
diff --git a/gno.land/pkg/gnoland/types_test.go b/gno.land/pkg/gnoland/types_test.go
new file mode 100644
index 00000000000..c501325bc3e
--- /dev/null
+++ b/gno.land/pkg/gnoland/types_test.go
@@ -0,0 +1,158 @@
+package gnoland
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
+ "github.com/gnolang/gno/tm2/pkg/amino"
+ "github.com/gnolang/gno/tm2/pkg/crypto"
+ "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1"
+ "github.com/gnolang/gno/tm2/pkg/sdk/bank"
+ "github.com/gnolang/gno/tm2/pkg/std"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// generateTxs generates dummy transactions
+func generateTxs(t *testing.T, count int) []TxWithMetadata {
+ t.Helper()
+
+ txs := make([]TxWithMetadata, count)
+
+ for i := 0; i < count; i++ {
+ txs[i] = TxWithMetadata{
+ Tx: std.Tx{
+ Msgs: []std.Msg{
+ bank.MsgSend{
+ FromAddress: crypto.Address{byte(i)},
+ ToAddress: crypto.Address{byte(i)},
+ Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 1)),
+ },
+ },
+ Fee: std.Fee{
+ GasWanted: 10,
+ GasFee: std.NewCoin(ugnot.Denom, 1000000),
+ },
+ Memo: fmt.Sprintf("tx %d", i),
+ },
+ }
+ }
+
+ return txs
+}
+
+func TestReadGenesisTxs(t *testing.T) {
+ t.Parallel()
+
+ createFile := func(path, data string) {
+ file, err := os.Create(path)
+ require.NoError(t, err)
+
+ _, err = file.WriteString(data)
+ require.NoError(t, err)
+ }
+
+ t.Run("invalid path", func(t *testing.T) {
+ t.Parallel()
+
+ path := "" // invalid
+
+ ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancelFn()
+
+ txs, err := ReadGenesisTxs(ctx, path)
+ assert.Nil(t, txs)
+
+ assert.Error(t, err)
+ })
+
+ t.Run("invalid tx format", func(t *testing.T) {
+ t.Parallel()
+
+ var (
+ dir = t.TempDir()
+ path = filepath.Join(dir, "txs.jsonl")
+ )
+
+ // Create the file
+ createFile(
+ path,
+ "random data",
+ )
+
+ ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancelFn()
+
+ txs, err := ReadGenesisTxs(ctx, path)
+ assert.Nil(t, txs)
+
+ assert.Error(t, err)
+ })
+
+ t.Run("valid txs", func(t *testing.T) {
+ t.Parallel()
+
+ var (
+ dir = t.TempDir()
+ path = filepath.Join(dir, "txs.jsonl")
+ txs = generateTxs(t, 1000)
+ )
+
+ // Create the file
+ file, err := os.Create(path)
+ require.NoError(t, err)
+
+ // Write the transactions
+ for _, tx := range txs {
+ encodedTx, err := amino.MarshalJSON(tx)
+ require.NoError(t, err)
+
+ _, err = file.WriteString(fmt.Sprintf("%s\n", encodedTx))
+ require.NoError(t, err)
+ }
+
+ ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancelFn()
+
+ // Load the transactions
+ readTxs, err := ReadGenesisTxs(ctx, path)
+ require.NoError(t, err)
+
+ require.Len(t, readTxs, len(txs))
+
+ for index, readTx := range readTxs {
+ assert.Equal(t, txs[index], readTx)
+ }
+ })
+}
+
+func TestSignGenesisTx(t *testing.T) {
+ t.Parallel()
+
+ var (
+ txs = generateTxs(t, 100)
+ privKey = secp256k1.GenPrivKey()
+ pubKey = privKey.PubKey()
+ chainID = "testing"
+ )
+
+ // Make sure the transactions are properly signed
+ require.NoError(t, SignGenesisTxs(txs, privKey, chainID))
+
+ // Make sure the signatures are valid
+ for _, tx := range txs {
+ payload, err := tx.Tx.GetSignBytes(chainID, 0, 0)
+ require.NoError(t, err)
+
+ sigs := tx.Tx.GetSignatures()
+ require.Len(t, sigs, 1)
+
+ assert.True(t, pubKey.Equals(sigs[0].PubKey))
+ assert.True(t, pubKey.VerifyBytes(payload, sigs[0].Signature))
+ }
+}
diff --git a/gno.land/pkg/gnoland/validators.go b/gno.land/pkg/gnoland/validators.go
new file mode 100644
index 00000000000..339ebd9dcad
--- /dev/null
+++ b/gno.land/pkg/gnoland/validators.go
@@ -0,0 +1,61 @@
+package gnoland
+
+import (
+ "regexp"
+
+ gnovm "github.com/gnolang/gno/gnovm/stdlibs/std"
+ "github.com/gnolang/gno/tm2/pkg/bft/types"
+ "github.com/gnolang/gno/tm2/pkg/events"
+)
+
+const (
+ valRealm = "gno.land/r/sys/validators/v2" // XXX: make it configurable from GovDAO
+ valChangesFn = "GetChanges"
+
+ validatorAddedEvent = "ValidatorAdded"
+ validatorRemovedEvent = "ValidatorRemoved"
+)
+
+// XXX: replace with amino-based clean approach
+var valRegexp = regexp.MustCompile(`{\("([^"]*)"\s[^)]+\),\("((?:[^"]|\\")*)"\s[^)]+\),\((\d+)\s[^)]+\)}`)
+
+// validatorUpdate is a type being used for "notifying"
+// that a validator change happened on-chain. The events from `r/sys/validators`
+// do not pass data related to validator add / remove instances (who, what, how)
+type validatorUpdate struct{}
+
+// validatorEventFilter filters the given event to determine if it
+// is tied to a validator update
+func validatorEventFilter(event events.Event) []validatorUpdate {
+ // Make sure the event is a new TX event
+ txResult, ok := event.(types.EventTx)
+ if !ok {
+ return nil
+ }
+
+ // Make sure an add / remove event happened
+ for _, ev := range txResult.Result.Response.Events {
+ // Make sure the event is a GnoVM event
+ gnoEv, ok := ev.(gnovm.GnoEvent)
+ if !ok {
+ continue
+ }
+
+ // Make sure the event is from `r/sys/validators`
+ if gnoEv.PkgPath != valRealm {
+ continue
+ }
+
+ // Make sure the event is either an add / remove
+ switch gnoEv.Type {
+ case validatorAddedEvent, validatorRemovedEvent:
+ // We don't pass data around with the events, but a single
+ // notification is enough to "trigger" a VM scrape
+ return []validatorUpdate{{}}
+ default:
+ continue
+ }
+ }
+
+ return nil
+}
diff --git a/gno.land/pkg/gnoland/vals.go b/gno.land/pkg/gnoland/vals.go
deleted file mode 100644
index 1843dff3984..00000000000
--- a/gno.land/pkg/gnoland/vals.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package gnoland
-
-import (
- "regexp"
-
- gnovm "github.com/gnolang/gno/gnovm/stdlibs/std"
- "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/events"
-)
-
-const (
- valRealm = "gno.land/r/sys/validators"
- valChangesFn = "GetChanges"
-
- validatorAddedEvent = "ValidatorAdded"
- validatorRemovedEvent = "ValidatorRemoved"
-)
-
-// XXX: replace with amino-based clean approach
-var valRegexp = regexp.MustCompile(`{\("([^"]*)"\s[^)]+\),\("((?:[^"]|\\")*)"\s[^)]+\),\((\d+)\s[^)]+\)}`)
-
-// validatorUpdate is a type being used for "notifying"
-// that a validator change happened on-chain. The events from `r/sys/validators`
-// do not pass data related to validator add / remove instances (who, what, how)
-type validatorUpdate struct{}
-
-// validatorEventFilter filters the given event to determine if it
-// is tied to a validator update
-func validatorEventFilter(event events.Event) []validatorUpdate {
- // Make sure the event is a new TX event
- txResult, ok := event.(types.EventTx)
- if !ok {
- return nil
- }
-
- // Make sure an add / remove event happened
- for _, ev := range txResult.Result.Response.Events {
- // Make sure the event is a GnoVM event
- gnoEv, ok := ev.(gnovm.GnoEvent)
- if !ok {
- continue
- }
-
- // Make sure the event is from `r/sys/validators`
- if gnoEv.PkgPath != valRealm {
- continue
- }
-
- // Make sure the event is either an add / remove
- switch gnoEv.Type {
- case validatorAddedEvent, validatorRemovedEvent:
- // We don't pass data around with the events, but a single
- // notification is enough to "trigger" a VM scrape
- return []validatorUpdate{{}}
- default:
- continue
- }
- }
-
- return nil
-}
diff --git a/gno.land/pkg/gnoweb/.gitignore b/gno.land/pkg/gnoweb/.gitignore
new file mode 100644
index 00000000000..dd09eb49099
--- /dev/null
+++ b/gno.land/pkg/gnoweb/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+tmp/
+.cache
diff --git a/gno.land/pkg/gnoweb/Makefile b/gno.land/pkg/gnoweb/Makefile
new file mode 100644
index 00000000000..8e8b6bf1a2c
--- /dev/null
+++ b/gno.land/pkg/gnoweb/Makefile
@@ -0,0 +1,104 @@
+# Configurable arguments
+DEV_REMOTE ?= 127.0.0.1:26657
+CHAIN_ID ?= test3
+PUBLIC_DIR ?= public
+
+# Variable Declarations
+tools_run := go run -modfile ./tools/go.mod
+run_reflex := $(tools_run) github.com/cespare/reflex
+run_logname := go -C ./tools run ./cmd/logname
+
+# css config
+input_css := frontend/css/input.css
+output_css := $(PUBLIC_DIR)/styles.css
+tw_version := 3.4.14
+tw_config_path := frontend/css/tx.config.js
+templates_files := $(shell find . -iname '*.gohtml')
+
+# static config
+src_dir_static := frontend/static
+out_dir_static := $(PUBLIC_DIR)
+input_static := $(shell find $(src_dir_static) -type f)
+output_static := $(patsubst $(src_dir_static)/%, $(out_dir_static)/%, $(input_static))
+
+# esbuild config
+src_dir_js := frontend/js
+out_dir_js := $(PUBLIC_DIR)/js
+input_js := $(shell find $(src_dir_js) -name '*.ts')
+output_js := $(patsubst $(src_dir_js)/%.ts,$(out_dir_js)/%.js,$(input_js))
+esbuild_version := 0.24.0
+
+# cache
+cache_dir := .cache
+
+#############
+# Targets
+#############
+.PHONY: all generate fmt css ts
+
+# Install dependencies
+all: generate
+
+test:
+ go test -v ./...
+
+# Generate process
+generate: css ts static
+
+css: $(output_css)
+$(output_css): $(input_css) $(templates_files)
+ npx -y tailwindcss@$(tw_version) -c $(tw_config_path) -i $(input_css) -o $@ --minify # tailwind
+ touch $@
+
+ts: $(output_js)
+$(out_dir_js)/%.js: $(src_dir_js)/%.ts
+ npx -y esbuild $< --log-level=error --bundle --outdir=$(out_dir_js) --format=esm --minify
+
+# Rule to copy static files while preserving directory structure
+static: $(output_static)
+$(out_dir_static)/%: $(src_dir_static)/%
+ @mkdir -p $(dir $@)
+ @cp -v $< $@
+
+# Format process
+fmt:
+ go fmt ./...
+
+ ###############################
+ # Developments
+ ###############################
+.PHONY: dev dev.server dev.css dev.ts deps
+
+# Run the development dependencies in parallel
+dev:
+ @echo "-- starting development tools"
+ @PUBLIC_DIR=$(cache_dir)/public $(MAKE) -j 3 \
+ dev.gnoweb \
+ dev.ts \
+ dev.css
+
+# Go server in development mode
+dev.gnoweb: generate
+ $(run_reflex) -s -r '.*\.go(html)?' -- \
+ go run ../../cmd/gnoweb -assets-dir=${PUBLIC_DIR} -chainid=${CHAIN_ID} -remote=${DEV_REMOTE} \
+ 2>&1 | $(run_logname) gnoweb
+
+# Tailwind CSS in development mode
+dev.css: generate | $(PUBLIC_DIR)
+ npx -y tailwindcss@$(tw_version) -c $(tw_config_path) --verbose -i $(input_css) -o $(output_css) --watch \
+ 2>&1 | $(run_logname) tailwind
+
+# XXX: add versioning on esbuild
+# TS in development mode
+dev.ts: generate | $(PUBLIC_DIR)
+ npx -y esbuild@$(esbuild_version) $(input_js) --bundle --outdir=$(out_dir_js) --sourcemap --format=esm --watch \
+ 2>&1 | $(run_logname) esbuild
+
+# Cleanup
+clean:
+ rm -rf $(cache_dir) tmp
+fclean: clean
+ rm -rf $(PUBLIC_DIR)
+
+# Dirs
+$(PUBLIC_DIR):; mkdir -p $@
diff --git a/gno.land/pkg/gnoweb/README.md b/gno.land/pkg/gnoweb/README.md
new file mode 100644
index 00000000000..287279538d8
--- /dev/null
+++ b/gno.land/pkg/gnoweb/README.md
@@ -0,0 +1,45 @@
+# gnoweb
+
+`gnoweb` is a universal web frontend for the gno.land blockchain.
+
+This README provides instructions on how to set up and run `gnoweb` for development purposes.
+
+## Prerequisites
+
+Before you begin, ensure you have the following software installed on your machine:
+
+- **Node.js**: Required for running JavaScript and CSS build tools.
+- **Go**: Required for building `gnoweb`
+
+## Development
+
+To start the development environment, which runs multiple development tools in parallel,
+use the following command:
+
+```sh
+make dev
+```
+
+This will:
+
+- Start a Go server in development mode and watch for any Go files change (targeting [localhost](http://localhost:8888)).
+- Enable Tailwind CSS in watch mode to automatically compile CSS changes.
+- Use esbuild in watch mode to automatically transpile and bundle TypeScript changes.
+
+You can customize the behavior of the Go server using the `DEV_REMOTE` and
+`CHAIN_ID` environment variables. For example, to use `portal-loop` as the
+target, run:
+
+```sh
+CHAIN_ID=portal-loop DEV_REMOTE=https://rpc.gno.land make dev
+```
+
+## Generate
+
+To generate the public assets for the project, including static assets (fonts, CSS and JavaScript...
+files), run the following command. This should be used while editing CSS, JS, or
+any asset files:
+
+```sh
+make generate
+```
diff --git a/gno.land/pkg/gnoweb/alias.go b/gno.land/pkg/gnoweb/alias.go
index d7297ed9d5d..06bb3941e41 100644
--- a/gno.land/pkg/gnoweb/alias.go
+++ b/gno.land/pkg/gnoweb/alias.go
@@ -1,26 +1,57 @@
package gnoweb
-// realm aliases
+import (
+ "net/http"
+
+ "github.com/gnolang/gno/gno.land/pkg/gnoweb/components"
+)
+
+// Aliases are gnoweb paths that are rewritten using [AliasAndRedirectMiddleware].
var Aliases = map[string]string{
- "/": "/r/gnoland/home",
- "/about": "/r/gnoland/pages:p/about",
- "/gnolang": "/r/gnoland/pages:p/gnolang",
- "/ecosystem": "/r/gnoland/pages:p/ecosystem",
- "/partners": "/r/gnoland/pages:p/partners",
- "/testnets": "/r/gnoland/pages:p/testnets",
- "/start": "/r/gnoland/pages:p/start",
- "/license": "/r/gnoland/pages:p/license",
- "/game-of-realms": "/r/gnoland/pages:p/gor", // XXX: replace with gor realm
- "/events": "/r/gnoland/events",
+ "/": "/r/gnoland/home",
+ "/about": "/r/gnoland/pages:p/about",
+ "/gnolang": "/r/gnoland/pages:p/gnolang",
+ "/ecosystem": "/r/gnoland/pages:p/ecosystem",
+ "/partners": "/r/gnoland/pages:p/partners",
+ "/testnets": "/r/gnoland/pages:p/testnets",
+ "/start": "/r/gnoland/pages:p/start",
+ "/license": "/r/gnoland/pages:p/license",
+ "/contribute": "/r/gnoland/pages:p/contribute",
+ "/events": "/r/gnoland/events",
}
-// http redirects
+// Redirect are gnoweb paths that are redirected using [AliasAndRedirectMiddleware].
var Redirects = map[string]string{
"/r/demo/boards:gnolang/6": "/r/demo/boards:gnolang/3", // XXX: temporary
"/blog": "/r/gnoland/blog",
- "/gor": "/game-of-realms",
+ "/gor": "/contribute",
+ "/game-of-realms": "/contribute",
"/grants": "/partners",
"/language": "/gnolang",
"/getting-started": "/start",
- "/gophercon24": "https://docs.gno.land",
+}
+
+// AliasAndRedirectMiddleware redirects all incoming requests whose path matches
+// any of the [Redirects] to the corresponding URL; and rewrites the URL path
+// for incoming requests which match any of the [Aliases].
+func AliasAndRedirectMiddleware(next http.Handler, analytics bool) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Check if the request path matches a redirect
+ if newPath, ok := Redirects[r.URL.Path]; ok {
+ http.Redirect(w, r, newPath, http.StatusFound)
+ components.RenderRedirectComponent(w, components.RedirectData{
+ To: newPath,
+ WithAnalytics: analytics,
+ })
+ return
+ }
+
+ // Check if the request path matches an alias
+ if newPath, ok := Aliases[r.URL.Path]; ok {
+ r.URL.Path = newPath
+ }
+
+ // Call the next handler
+ next.ServeHTTP(w, r)
+ })
}
diff --git a/gno.land/pkg/gnoweb/app.go b/gno.land/pkg/gnoweb/app.go
new file mode 100644
index 00000000000..516d3b92186
--- /dev/null
+++ b/gno.land/pkg/gnoweb/app.go
@@ -0,0 +1,169 @@
+package gnoweb
+
+import (
+ "fmt"
+ "log/slog"
+ "net/http"
+ "path"
+ "strings"
+
+ markdown "github.com/yuin/goldmark-highlighting/v2"
+
+ "github.com/alecthomas/chroma/v2"
+ chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
+ "github.com/alecthomas/chroma/v2/styles"
+ "github.com/gnolang/gno/gno.land/pkg/gnoweb/components"
+ "github.com/gnolang/gno/tm2/pkg/bft/rpc/client"
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/extension"
+ mdhtml "github.com/yuin/goldmark/renderer/html"
+)
+
+// AppConfig contains configuration for the gnoweb.
+type AppConfig struct {
+ // UnsafeHTML, if enabled, allows to use HTML in the markdown.
+ UnsafeHTML bool
+ // Analytics enables SimpleAnalytics.
+ Analytics bool
+ // NodeRemote is the remote address of the gno.land node.
+ NodeRemote string
+ // RemoteHelp is the remote of the gno.land node, as used in the help page.
+ RemoteHelp string
+ // ChainID is the chain id, used for constructing the help page.
+ ChainID string
+ // AssetsPath is the base path to the gnoweb assets.
+ AssetsPath string
+ // AssetDir, if set, will be used for assets instead of the embedded public directory.
+ AssetsDir string
+ // FaucetURL, if specified, will be the URL to which `/faucet` redirects.
+ FaucetURL string
+ // Domain is the domain used by the node.
+ Domain string
+}
+
+// NewDefaultAppConfig returns a new default [AppConfig]. The default sets
+// 127.0.0.1:26657 as the remote node, "dev" as the chain ID and sets up Assets
+// to be served on /public/.
+func NewDefaultAppConfig() *AppConfig {
+ const defaultRemote = "127.0.0.1:26657"
+ return &AppConfig{
+ NodeRemote: defaultRemote,
+ RemoteHelp: defaultRemote,
+ ChainID: "dev",
+ AssetsPath: "/public/",
+ Domain: "gno.land",
+ }
+}
+
+var chromaDefaultStyle = mustGetStyle("friendly")
+
+func mustGetStyle(name string) *chroma.Style {
+ s := styles.Get(name)
+ if s == nil {
+ panic("unable to get chroma style")
+ }
+ return s
+}
+
+// NewRouter initializes the gnoweb router with the specified logger and configuration.
+func NewRouter(logger *slog.Logger, cfg *AppConfig) (http.Handler, error) {
+ // Initialize RPC Client
+ client, err := client.NewHTTPClient(cfg.NodeRemote)
+ if err != nil {
+ return nil, fmt.Errorf("unable to create HTTP client: %w", err)
+ }
+
+ // Configure Chroma highlighter
+ chromaOptions := []chromahtml.Option{
+ chromahtml.WithLineNumbers(true),
+ chromahtml.WithLinkableLineNumbers(true, "L"),
+ chromahtml.WithClasses(true),
+ chromahtml.ClassPrefix("chroma-"),
+ }
+ chroma := chromahtml.New(chromaOptions...)
+
+ // Configure Goldmark markdown parser
+ mdopts := []goldmark.Option{
+ goldmark.WithExtensions(
+ markdown.NewHighlighting(
+ markdown.WithFormatOptions(chromaOptions...),
+ ),
+ extension.Table,
+ ),
+ }
+ if cfg.UnsafeHTML {
+ mdopts = append(mdopts, goldmark.WithRendererOptions(mdhtml.WithXHTML(), mdhtml.WithUnsafe()))
+ }
+ md := goldmark.New(mdopts...)
+
+ // Configure WebClient
+ webcfg := HTMLWebClientConfig{
+ Markdown: md,
+ Highlighter: NewChromaSourceHighlighter(chroma, chromaDefaultStyle),
+ Domain: cfg.Domain,
+ UnsafeHTML: cfg.UnsafeHTML,
+ RPCClient: client,
+ }
+
+ webcli := NewHTMLClient(logger, &webcfg)
+ chromaStylePath := path.Join(cfg.AssetsPath, "_chroma", "style.css")
+
+ // Setup StaticMetadata
+ staticMeta := StaticMetadata{
+ Domain: cfg.Domain,
+ AssetsPath: cfg.AssetsPath,
+ ChromaPath: chromaStylePath,
+ RemoteHelp: cfg.RemoteHelp,
+ ChainId: cfg.ChainID,
+ Analytics: cfg.Analytics,
+ }
+
+ // Configure WebHandler
+ webConfig := WebHandlerConfig{WebClient: webcli, Meta: staticMeta}
+ webhandler, err := NewWebHandler(logger, webConfig)
+ if err != nil {
+ return nil, fmt.Errorf("unable to create web handler: %w", err)
+ }
+
+ // Setup HTTP muxer
+ mux := http.NewServeMux()
+
+ // Handle web handler with alias middleware
+ mux.Handle("/", AliasAndRedirectMiddleware(webhandler, cfg.Analytics))
+
+ // Register faucet URL to `/faucet` if specified
+ if cfg.FaucetURL != "" {
+ mux.Handle("/faucet", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ http.Redirect(w, r, cfg.FaucetURL, http.StatusFound)
+ components.RenderRedirectComponent(w, components.RedirectData{
+ To: cfg.FaucetURL,
+ WithAnalytics: cfg.Analytics,
+ })
+ }))
+ }
+
+ // Handle Chroma CSS requests
+ // XXX: probably move this elsewhere
+ mux.Handle(chromaStylePath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/css")
+ if err := chroma.WriteCSS(w, chromaDefaultStyle); err != nil {
+ logger.Error("unable to write CSS", "err", err)
+ http.NotFound(w, r)
+ }
+ }))
+
+ // Handle assets path
+ // XXX: add caching
+ assetsBase := "/" + strings.Trim(cfg.AssetsPath, "/") + "/"
+ if cfg.AssetsDir != "" {
+ logger.Debug("using assets dir instead of embedded assets", "dir", cfg.AssetsDir)
+ mux.Handle(assetsBase, DevAssetHandler(assetsBase, cfg.AssetsDir))
+ } else {
+ mux.Handle(assetsBase, AssetHandler())
+ }
+
+ // Handle status page
+ mux.Handle("/status.json", handlerStatusJSON(logger, client))
+
+ return mux, nil
+}
diff --git a/gno.land/pkg/gnoweb/app_test.go b/gno.land/pkg/gnoweb/app_test.go
new file mode 100644
index 00000000000..9f8f87b99b1
--- /dev/null
+++ b/gno.land/pkg/gnoweb/app_test.go
@@ -0,0 +1,159 @@
+package gnoweb
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gnolang/gno/gno.land/pkg/integration"
+ "github.com/gnolang/gno/gnovm/pkg/gnoenv"
+ "github.com/gnolang/gno/tm2/pkg/log"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRoutes(t *testing.T) {
+ const (
+ ok = http.StatusOK
+ found = http.StatusFound
+ notFound = http.StatusNotFound
+ )
+ routes := []struct {
+ route string
+ status int
+ substring string
+ }{
+ {"/", ok, "Welcome"}, // Check if / returns 200 (OK) and contains "Welcome".
+ {"/about", ok, "blockchain"},
+ {"/r/gnoland/blog", ok, ""}, // Any content
+ {"/r/gnoland/blog$help", ok, "AdminSetAdminAddr"},
+ {"/r/gnoland/blog/", ok, "admin.gno"},
+ {"/r/gnoland/blog/admin.gno", ok, ">func<"},
+ {"/r/gnoland/blog$help&func=Render", ok, "Render(path)"},
+ {"/r/gnoland/blog$help&func=Render&path=foo/bar", ok, `value="foo/bar"`},
+ // {"/r/gnoland/blog$help&func=NonExisting", ok, "NonExisting not found"}, // XXX(TODO)
+ {"/r/demo/users:administrator", ok, "address"},
+ {"/r/demo/users", ok, "moul"},
+ {"/r/demo/users/users.gno", ok, "// State"},
+ {"/r/demo/deep/very/deep", ok, "it works!"},
+ {"/r/demo/deep/very/deep?arg1=val1&arg2=val2", ok, "hi ?arg1=val1&arg2=val2"},
+ {"/r/demo/deep/very/deep:bob", ok, "hi bob"},
+ {"/r/demo/deep/very/deep:bob?arg1=val1&arg2=val2", ok, "hi bob?arg1=val1&arg2=val2"},
+ {"/r/demo/deep/very/deep$help", ok, "Render"},
+ {"/r/demo/deep/very/deep/", ok, "render.gno"},
+ {"/r/demo/deep/very/deep/render.gno", ok, ">package<"},
+ {"/contribute", ok, "Game of Realms"},
+ {"/game-of-realms", found, "/contribute"},
+ {"/gor", found, "/contribute"},
+ {"/blog", found, "/r/gnoland/blog"},
+ {"/r/not/found/", notFound, ""},
+ {"/404/not/found", notFound, ""},
+ {"/아스키문자가아닌경로", notFound, ""},
+ {"/%ED%85%8C%EC%8A%A4%ED%8A%B8", notFound, ""},
+ {"/グノー", notFound, ""},
+ {"/\u269B\uFE0F", notFound, ""}, // Unicode
+ {"/p/demo/flow/LICENSE", ok, "BSD 3-Clause"},
+ // Test assets
+ {"/public/styles.css", ok, ""},
+ {"/public/js/index.js", ok, ""},
+ {"/public/_chroma/style.css", ok, ""},
+ {"/public/imgs/gnoland.svg", ok, ""},
+ }
+
+ 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()
+
+ cfg := NewDefaultAppConfig()
+ cfg.NodeRemote = remoteAddr
+
+ logger := log.NewTestingLogger(t)
+
+ // Initialize the router with the current node's remote address
+ router, err := NewRouter(logger, cfg)
+ require.NoError(t, err)
+
+ for _, r := range routes {
+ t.Run(fmt.Sprintf("test route %s", r.route), func(t *testing.T) {
+ t.Logf("input: %q", r.route)
+ request := httptest.NewRequest(http.MethodGet, r.route, nil)
+ response := httptest.NewRecorder()
+ router.ServeHTTP(response, request)
+ assert.Equal(t, r.status, response.Code)
+ assert.Contains(t, response.Body.String(), r.substring)
+ })
+ }
+}
+
+func TestAnalytics(t *testing.T) {
+ routes := []string{
+ // Special realms
+ "/", // Home
+ "/about",
+ "/start",
+
+ // Redirects
+ "/game-of-realms",
+ "/getting-started",
+ "/blog",
+ "/boards",
+
+ // Realm, source, help page
+ "/r/gnoland/blog",
+ "/r/gnoland/blog/admin.gno",
+ "/r/demo/users:administrator",
+ "/r/gnoland/blog$help",
+
+ // Special pages
+ "/404-not-found",
+ }
+
+ 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()
+
+ t.Run("enabled", func(t *testing.T) {
+ for _, route := range routes {
+ t.Run(route, func(t *testing.T) {
+ cfg := NewDefaultAppConfig()
+ cfg.NodeRemote = remoteAddr
+ cfg.Analytics = true
+ logger := log.NewTestingLogger(t)
+
+ router, err := NewRouter(logger, cfg)
+ require.NoError(t, err)
+
+ request := httptest.NewRequest(http.MethodGet, route, nil)
+ response := httptest.NewRecorder()
+
+ router.ServeHTTP(response, request)
+
+ assert.Contains(t, response.Body.String(), "sa.gno.services")
+ })
+ }
+ })
+ t.Run("disabled", func(t *testing.T) {
+ for _, route := range routes {
+ t.Run(route, func(t *testing.T) {
+ cfg := NewDefaultAppConfig()
+ cfg.NodeRemote = remoteAddr
+ cfg.Analytics = false
+ logger := log.NewTestingLogger(t)
+ router, err := NewRouter(logger, cfg)
+ require.NoError(t, err)
+
+ request := httptest.NewRequest(http.MethodGet, route, nil)
+ response := httptest.NewRecorder()
+
+ router.ServeHTTP(response, request)
+
+ assert.NotContains(t, response.Body.String(), "sa.gno.services")
+ })
+ }
+ })
+}
diff --git a/gno.land/pkg/gnoweb/components/breadcrumb.go b/gno.land/pkg/gnoweb/components/breadcrumb.go
new file mode 100644
index 00000000000..8eda02a9f4d
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/breadcrumb.go
@@ -0,0 +1,19 @@
+package components
+
+import (
+ "io"
+)
+
+type BreadcrumbPart struct {
+ Name string
+ URL string
+}
+
+type BreadcrumbData struct {
+ Parts []BreadcrumbPart
+ Args string
+}
+
+func RenderBreadcrumpComponent(w io.Writer, data BreadcrumbData) error {
+ return tmpl.ExecuteTemplate(w, "Breadcrumb", data)
+}
diff --git a/gno.land/pkg/gnoweb/components/breadcrumb.gohtml b/gno.land/pkg/gnoweb/components/breadcrumb.gohtml
new file mode 100644
index 00000000000..3824eb5894f
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/breadcrumb.gohtml
@@ -0,0 +1,18 @@
+{{ define "breadcrumb" }}
+
+ {{- range $index, $part := .Parts }}
+ {{- if $index }}
+
+ {{- else }}
+
+ {{- end }}
+ {{ $part.Name }}
+
+ {{- end }}
+ {{- if .Args }}
+
+ {{ .Args }}
+
+ {{- end }}
+
+{{ end }}
diff --git a/gno.land/pkg/gnoweb/components/directory.go b/gno.land/pkg/gnoweb/components/directory.go
new file mode 100644
index 00000000000..6e47db3b2c4
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/directory.go
@@ -0,0 +1,15 @@
+package components
+
+import (
+ "io"
+)
+
+type DirData struct {
+ PkgPath string
+ Files []string
+ FileCounter int
+}
+
+func RenderDirectoryComponent(w io.Writer, data DirData) error {
+ return tmpl.ExecuteTemplate(w, "renderDir", data)
+}
diff --git a/gno.land/pkg/gnoweb/components/directory.gohtml b/gno.land/pkg/gnoweb/components/directory.gohtml
new file mode 100644
index 00000000000..2254886f7af
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/directory.gohtml
@@ -0,0 +1,38 @@
+{{ define "renderDir" }}
+
+
+
+ {{ $pkgpath := .PkgPath }}
+
+
+
+
{{ $pkgpath }}
+
+
+ Directory · {{ .FileCounter }} Files
+
+
+
+
+
+
+
+
+{{ end }}
+
diff --git a/gno.land/pkg/gnoweb/components/help.go b/gno.land/pkg/gnoweb/components/help.go
new file mode 100644
index 00000000000..e819705006b
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/help.go
@@ -0,0 +1,51 @@
+package components
+
+import (
+ "html/template"
+ "io"
+ "strings"
+
+ "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // for error types
+)
+
+type HelpData struct {
+ // Selected function
+ SelectedFunc string
+ SelectedArgs map[string]string
+
+ RealmName string
+ Functions []vm.FunctionSignature
+ ChainId string
+ Remote string
+ PkgPath string
+}
+
+func registerHelpFuncs(funcs template.FuncMap) {
+ funcs["helpFuncSignature"] = func(fsig vm.FunctionSignature) (string, error) {
+ var fsigStr strings.Builder
+
+ fsigStr.WriteString(fsig.FuncName)
+ fsigStr.WriteRune('(')
+ for i, param := range fsig.Params {
+ if i > 0 {
+ fsigStr.WriteString(", ")
+ }
+ fsigStr.WriteString(param.Name)
+ }
+ fsigStr.WriteRune(')')
+
+ return fsigStr.String(), nil
+ }
+
+ funcs["getSelectedArgValue"] = func(data HelpData, param vm.NamedType) (string, error) {
+ if data.SelectedArgs == nil {
+ return "", nil
+ }
+
+ return data.SelectedArgs[param.Name], nil
+ }
+}
+
+func RenderHelpComponent(w io.Writer, data HelpData) error {
+ return tmpl.ExecuteTemplate(w, "renderHelp", data)
+}
diff --git a/gno.land/pkg/gnoweb/components/help.gohtml b/gno.land/pkg/gnoweb/components/help.gohtml
new file mode 100644
index 00000000000..535cb56e9d6
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/help.gohtml
@@ -0,0 +1,110 @@
+{{ define "renderHelp" }}
+ {{ $data := . }}
+
+
+
+
+
+
+ {{ range .Functions }}
+
+ {{ .FuncName }}
+
+
+
Command
+
+
+
+
+
+
+
+
gnokey maketx call -pkgpath "{{ $.PkgPath }}" -func "{{ .FuncName }}" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid "{{ $.ChainId }}"{{ range .Params }} -args " "{{ end }} -remote "{{ $.Remote }}" ADDRESS gnokey query -remote "{{ $.Remote }}" auth/accounts/ADDRESS
+gnokey maketx call -pkgpath "{{ $.PkgPath }}" -func "{{ .FuncName }}" -gas-fee 1000000ugnot -gas-wanted 2000000 -send "" {{ range .Params }} -args " "{{ end }} ADDRESS > call.tx
+gnokey sign -tx-path call.tx -chainid "{{ $.ChainId }}" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER ADDRESS
+gnokey broadcast -remote "{{ $.Remote }}" call.tx
+
+
+
+ {{ end }}
+
+
+
+
+{{ end }}
diff --git a/gno.land/pkg/gnoweb/components/index.go b/gno.land/pkg/gnoweb/components/index.go
new file mode 100644
index 00000000000..0cc020ae261
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/index.go
@@ -0,0 +1,47 @@
+package components
+
+import (
+ "context"
+ "html/template"
+ "io"
+ "net/url"
+)
+
+type HeadData struct {
+ Title string
+ Description string
+ Canonical string
+ Image string
+ URL string
+ ChromaPath string
+ AssetsPath string
+ Analytics bool
+}
+
+type HeaderData struct {
+ RealmPath string
+ Breadcrumb BreadcrumbData
+ WebQuery url.Values
+}
+
+type FooterData struct {
+ Analytics bool
+ AssetsPath string
+}
+
+type IndexData struct {
+ HeadData
+ HeaderData
+ FooterData
+ Body template.HTML
+}
+
+func IndexComponent(data IndexData) Component {
+ return func(ctx context.Context, tmpl *template.Template, w io.Writer) error {
+ return tmpl.ExecuteTemplate(w, "index", data)
+ }
+}
+
+func RenderIndexComponent(w io.Writer, data IndexData) error {
+ return tmpl.ExecuteTemplate(w, "index", data)
+}
diff --git a/gno.land/pkg/gnoweb/components/index.gohtml b/gno.land/pkg/gnoweb/components/index.gohtml
new file mode 100644
index 00000000000..a87decc14bf
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/index.gohtml
@@ -0,0 +1,159 @@
+{{ define "index" }}
+
+
+ {{ template "head" .HeadData }}
+
+ {{ template "spritesvg" }}
+
+
+ {{ template "header" .HeaderData }}
+
+
+ {{ template "main" .Body }}
+
+
+ {{ template "footer" .FooterData }}
+
+
+{{ end }}
+
+{{ define "head" }}
+
+
+
+ {{ .Title }}
+
+
+
+
+
+
+
+
+
+
+ {{ if .Canonical }}
+
+ {{ end }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{ end }}
+
+{{ define "header" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{ end }}
+
+{{ define "main" }}
+ {{ . }}
+{{ end }}
+
+{{ define "footer" }}
+
+
+{{- if .Analytics -}} {{- template "analytics" }} {{- end -}}
+
+{{- end }}
+
+{{- define "analytics" -}}
+
+
+
+
+
+{{- end -}}
diff --git a/gno.land/pkg/gnoweb/components/logosvg.gohtml b/gno.land/pkg/gnoweb/components/logosvg.gohtml
new file mode 100644
index 00000000000..5ebe6460ee3
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/logosvg.gohtml
@@ -0,0 +1,21 @@
+{{ define "logosvg" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{ end }}
diff --git a/gno.land/pkg/gnoweb/components/realm.go b/gno.land/pkg/gnoweb/components/realm.go
new file mode 100644
index 00000000000..027760bb382
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/realm.go
@@ -0,0 +1,32 @@
+package components
+
+import (
+ "context"
+ "html/template"
+ "io"
+
+ "github.com/gnolang/gno/gno.land/pkg/gnoweb/markdown"
+)
+
+type RealmTOCData struct {
+ Items []*markdown.TocItem
+}
+
+func RealmTOCComponent(data *RealmTOCData) Component {
+ return func(ctx context.Context, tmpl *template.Template, w io.Writer) error {
+ return tmpl.ExecuteTemplate(w, "renderRealmToc", data)
+ }
+}
+
+func RenderRealmTOCComponent(w io.Writer, data *RealmTOCData) error {
+ return tmpl.ExecuteTemplate(w, "renderRealmToc", data)
+}
+
+type RealmData struct {
+ Content template.HTML
+ TocItems *RealmTOCData
+}
+
+func RenderRealmComponent(w io.Writer, data RealmData) error {
+ return tmpl.ExecuteTemplate(w, "renderRealm", data)
+}
diff --git a/gno.land/pkg/gnoweb/components/realm.gohtml b/gno.land/pkg/gnoweb/components/realm.gohtml
new file mode 100644
index 00000000000..55f39ef36d7
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/realm.gohtml
@@ -0,0 +1,41 @@
+{{ define "renderRealmToc" }}
+
+{{ end }}
+
+{{ define "renderRealm" }}
+
+
+
+
+
+ {{ .Content }}
+
+
+
+{{ end }}
diff --git a/gno.land/pkg/gnoweb/components/redirect.go b/gno.land/pkg/gnoweb/components/redirect.go
new file mode 100644
index 00000000000..873ddf56ff5
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/redirect.go
@@ -0,0 +1,12 @@
+package components
+
+import "io"
+
+type RedirectData struct {
+ To string
+ WithAnalytics bool
+}
+
+func RenderRedirectComponent(w io.Writer, data RedirectData) error {
+ return tmpl.ExecuteTemplate(w, "renderRedirect", data)
+}
diff --git a/gno.land/pkg/gnoweb/components/redirect.gohtml b/gno.land/pkg/gnoweb/components/redirect.gohtml
new file mode 100644
index 00000000000..45dac0981cd
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/redirect.gohtml
@@ -0,0 +1,16 @@
+{{- define "renderRedirect" -}}
+
+
+
+
+
+
+
+ Redirecting to {{.To}}
+
+
+ {{.To}}
+ {{- if .WithAnalytics -}} {{- template "analytics" }} {{- end -}}
+
+
+{{- end -}}
\ No newline at end of file
diff --git a/gno.land/pkg/gnoweb/components/source.go b/gno.land/pkg/gnoweb/components/source.go
new file mode 100644
index 00000000000..23170776657
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/source.go
@@ -0,0 +1,20 @@
+package components
+
+import (
+ "html/template"
+ "io"
+)
+
+type SourceData struct {
+ PkgPath string
+ Files []string
+ FileName string
+ FileSize string
+ FileLines int
+ FileCounter int
+ FileSource template.HTML
+}
+
+func RenderSourceComponent(w io.Writer, data SourceData) error {
+ return tmpl.ExecuteTemplate(w, "renderSource", data)
+}
diff --git a/gno.land/pkg/gnoweb/components/source.gohtml b/gno.land/pkg/gnoweb/components/source.gohtml
new file mode 100644
index 00000000000..cb2430b504a
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/source.gohtml
@@ -0,0 +1,57 @@
+{{ define "renderSource" }}
+
+
+
+
+
+
+
+ {{ .FileSource }}
+
+
+
+
+{{ end }}
diff --git a/gno.land/pkg/gnoweb/components/spritesvg.gohtml b/gno.land/pkg/gnoweb/components/spritesvg.gohtml
new file mode 100644
index 00000000000..c061e97bf58
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/spritesvg.gohtml
@@ -0,0 +1,125 @@
+{{ define "spritesvg" }}
+
+
+ Search
+
+
+
+
+
+
+ Apps
+
+
+
+ Documentation
+
+
+
+ Source
+
+
+
+ Content
+
+
+
+ File
+
+
+
+ Folder
+
+
+
+
+
+
+
+
+
+
+ Download
+
+
+
+ Copy
+
+
+
+
+
+
+
+
+
+
+{{ end }}
diff --git a/gno.land/pkg/gnoweb/components/status.gohtml b/gno.land/pkg/gnoweb/components/status.gohtml
new file mode 100644
index 00000000000..2321d1110bd
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/status.gohtml
@@ -0,0 +1,12 @@
+{{ define "status" }}
+
+
+
+
+
Error: {{ .Message }}
+
Something went wrong. Let’s find our way back!
+
Go Back Home
+
+
+
+{{ end }}
diff --git a/gno.land/pkg/gnoweb/components/template.go b/gno.land/pkg/gnoweb/components/template.go
new file mode 100644
index 00000000000..9c08703f460
--- /dev/null
+++ b/gno.land/pkg/gnoweb/components/template.go
@@ -0,0 +1,77 @@
+package components
+
+import (
+ "bytes"
+ "context"
+ "embed"
+ "html/template"
+ "io"
+ "net/url"
+)
+
+//go:embed *.gohtml
+var gohtml embed.FS
+
+var funcMap = template.FuncMap{
+ // NOTE: this method does NOT escape HTML, use with caution
+ "noescape_string": func(in string) template.HTML {
+ return template.HTML(in) //nolint:gosec
+ },
+ // NOTE: this method does NOT escape HTML, use with caution
+ "noescape_bytes": func(in []byte) template.HTML {
+ return template.HTML(in) //nolint:gosec
+ },
+ "queryHas": func(vals url.Values, key string) bool {
+ if vals == nil {
+ return false
+ }
+
+ return vals.Has(key)
+ },
+}
+
+var tmpl = template.New("web").Funcs(funcMap)
+
+func init() {
+ registerHelpFuncs(funcMap)
+ tmpl.Funcs(funcMap)
+
+ var err error
+ tmpl, err = tmpl.ParseFS(gohtml, "*.gohtml")
+ if err != nil {
+ panic("unable to parse embed tempalates: " + err.Error())
+ }
+}
+
+type Component func(ctx context.Context, tmpl *template.Template, w io.Writer) error
+
+func (c Component) Render(ctx context.Context, w io.Writer) error {
+ return RenderComponent(ctx, w, c)
+}
+
+func RenderComponent(ctx context.Context, w io.Writer, c Component) error {
+ var render *template.Template
+ funcmap := template.FuncMap{
+ "render": func(cf Component) (string, error) {
+ var buf bytes.Buffer
+ if err := cf(ctx, render, &buf); err != nil {
+ return "", err
+ }
+
+ return buf.String(), nil
+ },
+ }
+
+ render = tmpl.Funcs(funcmap)
+ return c(ctx, render, w)
+}
+
+type StatusData struct {
+ Message string
+}
+
+func RenderStatusComponent(w io.Writer, message string) error {
+ return tmpl.ExecuteTemplate(w, "status", StatusData{
+ Message: message,
+ })
+}
diff --git a/gno.land/pkg/gnoweb/format.go b/gno.land/pkg/gnoweb/format.go
new file mode 100644
index 00000000000..67911bfa985
--- /dev/null
+++ b/gno.land/pkg/gnoweb/format.go
@@ -0,0 +1,69 @@
+package gnoweb
+
+import (
+ "fmt"
+ "io"
+ "path/filepath"
+ "strings"
+
+ "github.com/alecthomas/chroma/v2"
+ "github.com/alecthomas/chroma/v2/formatters/html"
+ "github.com/alecthomas/chroma/v2/lexers"
+)
+
+// FormatSource defines the interface for formatting source code.
+type FormatSource interface {
+ Format(w io.Writer, fileName string, file []byte) error
+}
+
+// ChromaSourceHighlighter implements the Highlighter interface using the Chroma library.
+type ChromaSourceHighlighter struct {
+ *html.Formatter
+ style *chroma.Style
+}
+
+// NewChromaSourceHighlighter constructs a new ChromaHighlighter with the given formatter and style.
+func NewChromaSourceHighlighter(formatter *html.Formatter, style *chroma.Style) FormatSource {
+ return &ChromaSourceHighlighter{Formatter: formatter, style: style}
+}
+
+// Format applies syntax highlighting to the source code using Chroma.
+func (f *ChromaSourceHighlighter) Format(w io.Writer, fileName string, src []byte) error {
+ var lexer chroma.Lexer
+
+ // Determine the lexer to be used based on the file extension.
+ switch strings.ToLower(filepath.Ext(fileName)) {
+ case ".gno":
+ lexer = lexers.Get("go")
+ case ".md":
+ lexer = lexers.Get("markdown")
+ case ".mod":
+ lexer = lexers.Get("gomod")
+ default:
+ lexer = lexers.Get("txt") // Unsupported file type, default to plain text.
+ }
+
+ if lexer == nil {
+ return fmt.Errorf("unsupported lexer for file %q", fileName)
+ }
+
+ iterator, err := lexer.Tokenise(nil, string(src))
+ if err != nil {
+ return fmt.Errorf("unable to tokenise %q: %w", fileName, err)
+ }
+
+ if err := f.Formatter.Format(w, f.style, iterator); err != nil {
+ return fmt.Errorf("unable to format source file %q: %w", fileName, err)
+ }
+
+ return nil
+}
+
+// noopFormat is a no-operation highlighter that writes the source code as-is.
+type noopFormat struct{}
+
+// Format writes the source code to the writer without any formatting.
+func (f *noopFormat) Format(w io.Writer, fileName string, src []byte) error {
+ _, err := w.Write(src)
+ return err
+}
diff --git a/gno.land/pkg/gnoweb/frontend/css/input.css b/gno.land/pkg/gnoweb/frontend/css/input.css
new file mode 100644
index 00000000000..59e41ff4a7c
--- /dev/null
+++ b/gno.land/pkg/gnoweb/frontend/css/input.css
@@ -0,0 +1,336 @@
+@font-face {
+ font-family: "Roboto";
+ font-style: normal;
+ font-weight: 900;
+ font-display: swap;
+ src: url("./fonts/roboto/roboto-mono-normal.woff2") format("woff2"), url("./fonts/roboto/roboto-mono-normal.woff") format("woff");
+}
+
+@font-face {
+ font-family: "Inter var";
+ font-weight: 100 900;
+ font-display: block;
+ font-style: oblique 0deg 10deg;
+ src: url("./fonts/intervar/Intervar.woff2") format("woff2");
+}
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ html {
+ @apply font-interVar text-gray-600 bg-light text-200;
+ font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on;
+ -webkit-font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on;
+ text-size-adjust: 100%;
+ -moz-osx-font-smoothing: grayscale;
+ font-smoothing: antialiased;
+ font-variant-ligatures: contextual common-ligatures;
+ font-kerning: normal;
+ text-rendering: optimizeLegibility;
+ }
+
+ svg {
+ @apply max-w-full max-h-full;
+ }
+
+ form {
+ @apply my-0;
+ }
+
+ .realm-content {
+ @apply text-200 break-words pt-10;
+ }
+
+ .realm-content > *:first-child {
+ @apply !mt-0;
+ }
+
+ .realm-content a {
+ @apply text-green-600 font-medium hover:underline;
+ }
+
+ .realm-content h1,
+ .realm-content h2,
+ .realm-content h3,
+ .realm-content h4 {
+ @apply text-gray-900 mt-12 leading-tight;
+ }
+
+ .realm-content h2,
+ .realm-content h2 * {
+ @apply font-bold;
+ }
+
+ .realm-content h3,
+ .realm-content h3 *,
+ .realm-content h4,
+ .realm-content h4 * {
+ @apply font-semibold;
+ }
+
+ .realm-content h1 + h2,
+ .realm-content h2 + h3,
+ .realm-content h3 + h4 {
+ @apply mt-4;
+ }
+
+ .realm-content h1 {
+ @apply text-800 font-bold;
+ }
+
+ .realm-content h2 {
+ @apply text-600;
+ }
+
+ .realm-content h3 {
+ @apply text-400 text-gray-600 mt-10;
+ }
+
+ .realm-content h4 {
+ @apply text-300 text-gray-600 font-medium my-6;
+ }
+
+ .realm-content p {
+ @apply my-5;
+ }
+
+ .realm-content strong {
+ @apply font-bold text-gray-900;
+ }
+
+ .realm-content strong * {
+ @apply font-bold;
+ }
+
+ .realm-content em {
+ @apply italic-subtle;
+ }
+
+ .realm-content blockquote {
+ @apply border-l-4 border-gray-300 pl-4 text-gray-600 italic-subtle my-4;
+ }
+
+ .realm-content ul,
+ .realm-content ol {
+ @apply pl-4 my-6;
+ }
+
+ .realm-content ul li,
+ .realm-content ol li {
+ @apply mb-2;
+ }
+
+ .realm-content img {
+ @apply max-w-full my-8;
+ }
+
+ .realm-content figure {
+ @apply my-6 text-center;
+ }
+
+ .realm-content figcaption {
+ @apply text-100 text-gray-600;
+ }
+
+ .realm-content :not(pre) > code {
+ @apply bg-gray-100 px-1 py-0.5 rounded-sm text-[.96em] font-mono;
+ }
+
+ .realm-content pre {
+ @apply bg-gray-50 p-4 rounded overflow-x-auto font-mono;
+ }
+
+ .realm-content hr {
+ @apply border-t border-gray-100 my-10;
+ }
+
+ .realm-content table {
+ @apply border-collapse my-8 block w-full max-w-full overflow-x-auto border-collapse;
+ }
+
+ .realm-content th,
+ .realm-content td {
+ @apply border px-4 py-2 break-words whitespace-normal;
+ }
+
+ .realm-content th {
+ @apply bg-gray-100 font-bold;
+ }
+
+ .realm-content caption {
+ @apply mt-2 text-100 text-gray-600 text-left;
+ }
+
+ .realm-content q {
+ @apply quotes;
+ }
+
+ .realm-content q::before {
+ content: open-quote;
+ }
+
+ .realm-content q::after {
+ content: close-quote;
+ }
+
+ .realm-content ul ul,
+ .realm-content ul ol,
+ .realm-content ol ul,
+ .realm-content ol ol {
+ @apply mt-3 mb-2 pl-4;
+ }
+
+ .realm-content ul {
+ @apply list-disc;
+ }
+
+ .realm-content ol {
+ @apply list-decimal;
+ }
+
+ .realm-content abbr[title] {
+ @apply border-b border-dotted cursor-help;
+ }
+
+ .realm-content details {
+ @apply my-5;
+ }
+
+ .realm-content summary {
+ @apply font-bold cursor-pointer;
+ }
+
+ .realm-content a code {
+ @apply text-inherit;
+ }
+
+ .realm-content video {
+ @apply max-w-full my-8;
+ }
+
+ .realm-content math {
+ @apply font-mono;
+ }
+
+ .realm-content small {
+ @apply text-100;
+ }
+
+ .realm-content del {
+ @apply line-through;
+ }
+
+ .realm-content sub {
+ @apply text-50 align-sub;
+ }
+
+ .realm-content sup {
+ @apply text-50 align-super;
+ }
+
+ .realm-content input,
+ .realm-content button {
+ @apply px-4 py-2 border border-gray-300;
+ }
+
+ main :is(h1, h2, h3, h4) {
+ @apply scroll-mt-24;
+ }
+
+ ::-moz-selection {
+ @apply bg-green-600 text-light;
+ }
+ ::selection {
+ @apply bg-green-600 text-light;
+ }
+}
+
+@layer components {
+ /* header */
+ .sidemenu .peer:checked + label > svg {
+ @apply text-green-600;
+ }
+
+ /* toc */
+ .toc-expend-btn:has(#toc-expend:checked) + nav {
+ @apply block;
+ }
+ .toc-expend-btn:has(#toc-expend:checked) .toc-expend-btn_ico {
+ @apply rotate-180;
+ }
+
+ /* sidebar */
+ .main-header:has(#sidemenu-summary:checked) + main #sidebar #sidebar-summary,
+ .main-header:has(#sidemenu-source:checked) + main #sidebar #sidebar-source,
+ .main-header:has(#sidemenu-docs:checked) + main #sidebar #sidebar-docs,
+ .main-header:has(#sidemenu-meta:checked) + main #sidebar #sidebar-meta {
+ @apply block;
+ }
+
+ :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) + main .realm-content,
+ :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) .main-navigation {
+ @apply md:col-span-6;
+ }
+ :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) + main #sidebar,
+ :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) .sidemenu {
+ @apply md:col-span-4;
+ }
+ :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) + main #sidebar::before {
+ @apply absolute block content-[''] top-0 w-[50vw] h-full -left-7 bg-gray-100 z-min;
+ }
+
+ /* chroma */
+ main :is(.source-code) > pre {
+ @apply !bg-light overflow-scroll rounded py-4 md:py-8 px-1 md:px-3 font-mono text-100 md:text-200;
+ }
+ main .realm-content > pre a {
+ @apply hover:no-underline;
+ }
+
+ main :is(.realm-content, .source-code) > pre .chroma-ln:target {
+ @apply !bg-transparent;
+ }
+ main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-ln:target),
+ main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-lnlinks:hover),
+ main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-ln:target) .chroma-cl,
+ main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-lnlinks:hover) .chroma-cl {
+ @apply !bg-gray-100 rounded;
+ }
+ main :is(.realm-content, .source-code) > pre .chroma-ln {
+ @apply scroll-mt-24;
+ }
+}
+
+@layer utilities {
+ .italic-subtle {
+ font-style: oblique 10deg;
+ }
+
+ .quotes {
+ @apply italic-subtle text-[#555] border-l-4 border-l-[#ccc] pl-4 my-6 [quotes:"“"_"”"_"‘"_"’"];
+ }
+
+ .quotes::before,
+ .quotes::after {
+ @apply [content:open-quote] text-600 text-gray-300 mr-1 [vertical-align:-0.4rem];
+ }
+
+ .quotes::after {
+ @apply [content:close-quote];
+ }
+
+ .text-stroke {
+ -webkit-text-stroke: currentColor;
+ -webkit-text-stroke-width: 0.6px;
+ }
+
+ .no-scrollbar::-webkit-scrollbar {
+ display: none;
+ }
+ .no-scrollbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ }
+}
diff --git a/gno.land/pkg/gnoweb/frontend/css/tx.config.js b/gno.land/pkg/gnoweb/frontend/css/tx.config.js
new file mode 100644
index 00000000000..21b6a101dd6
--- /dev/null
+++ b/gno.land/pkg/gnoweb/frontend/css/tx.config.js
@@ -0,0 +1,72 @@
+const pxToRem = (px) => px / 16;
+
+export default {
+ content: ["./components/**/*.{gohtml,ts}"],
+ theme: {
+ screens: {
+ xs: `${pxToRem(360)}rem`,
+ sm: `${pxToRem(480)}rem`,
+ md: `${pxToRem(640)}rem`,
+ lg: `${pxToRem(820)}rem`,
+ xl: `${pxToRem(1020)}rem`,
+ xxl: `${pxToRem(1366)}rem`,
+ max: `${pxToRem(1580)}rem`,
+ },
+ zIndex: {
+ min: "-1",
+ 1: "1",
+ 2: "2",
+ 100: "100",
+ max: "9999",
+ },
+ container: {
+ center: true,
+ padding: `${pxToRem(40)}rem`,
+ },
+ borderRadius: {
+ sm: `${pxToRem(4)}rem`,
+ DEFAULT: `${pxToRem(6)}rem`,
+ },
+ colors: {
+ light: "#FFFFFF",
+ gray: {
+ 50: "#F0F0F0", // Background color
+ 100: "#E2E2E2", // Title dark color
+ 200: "#BDBDBD", // Content dark color
+ 300: "#999999", // Muted color
+ 400: "#7C7C7C", // Border color
+ 600: "#54595D", // Content color
+ 800: "#131313", // Background dark color
+ 900: "#080809", // Title color
+ },
+ green: {
+ 400: "#2D8D72", // Primary dark color
+ 600: "#226C57", // Primary light color
+ },
+ transparent: "transparent",
+ current: "currentColor",
+ inherit: "inherit",
+ },
+ fontFamily: {
+ mono: ["Roboto", 'Menlo, Consolas, "Ubuntu Mono", "Roboto Mono", "DejaVu Sans Mono", monospace;'],
+ interVar: [
+ '"Inter var"',
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif',
+ ],
+ },
+ fontSize: {
+ 0: "0",
+ 50: `${pxToRem(12)}rem`,
+ 100: `${pxToRem(14)}rem`,
+ 200: `${pxToRem(16)}rem`,
+ 300: `${pxToRem(18)}rem`,
+ 400: `${pxToRem(20)}rem`,
+ 500: `${pxToRem(22)}rem`,
+ 600: `${pxToRem(24)}rem`,
+ 700: `${pxToRem(32)}rem`,
+ 800: `${pxToRem(38)}rem`,
+ 900: `${pxToRem(42)}rem`,
+ },
+ },
+ plugins: [],
+};
diff --git a/gno.land/pkg/gnoweb/frontend/js/copy.ts b/gno.land/pkg/gnoweb/frontend/js/copy.ts
new file mode 100644
index 00000000000..f3e5c725783
--- /dev/null
+++ b/gno.land/pkg/gnoweb/frontend/js/copy.ts
@@ -0,0 +1,105 @@
+class Copy {
+ private DOM: {
+ el: HTMLElement | null;
+ };
+ private static FEEDBACK_DELAY = 750;
+
+ private btnClicked: HTMLElement | null = null;
+ private btnClickedIcons: HTMLElement[] = [];
+ private isAnimationRunning: boolean = false;
+
+ private static SELECTORS = {
+ button: "[data-copy-btn]",
+ icon: `[data-copy-icon] > use`,
+ content: (id: string) => `[data-copy-content="${id}"]`,
+ };
+
+ constructor() {
+ this.DOM = {
+ el: document.querySelector("main"),
+ };
+
+ if (this.DOM.el) {
+ this.init();
+ } else {
+ console.warn("Copy: Main container not found.");
+ }
+ }
+
+ private init(): void {
+ this.bindEvents();
+ }
+
+ private bindEvents(): void {
+ this.DOM.el?.addEventListener("click", this.handleClick.bind(this));
+ }
+
+ private handleClick(event: Event): void {
+ const target = event.target as HTMLElement;
+ const button = target.closest(Copy.SELECTORS.button);
+
+ if (!button) return;
+
+ this.btnClicked = button;
+ this.btnClickedIcons = Array.from(button.querySelectorAll(Copy.SELECTORS.icon));
+
+ const contentId = button.getAttribute("data-copy-btn");
+ if (!contentId) {
+ console.warn("Copy: No content ID found on the button.");
+ return;
+ }
+
+ const codeBlock = this.DOM.el?.querySelector(Copy.SELECTORS.content(contentId));
+ if (codeBlock) {
+ this.copyToClipboard(codeBlock, this.btnClickedIcons);
+ } else {
+ console.warn(`Copy: No content found for ID "${contentId}".`);
+ }
+ }
+
+ private sanitizeContent(codeBlock: HTMLElement): string {
+ const html = codeBlock.innerHTML.replace(/]*class="chroma-ln"[^>]*>[\s\S]*?<\/span>/g, "");
+
+ const tempDiv = document.createElement("div");
+ tempDiv.innerHTML = html;
+
+ return tempDiv.textContent?.trim() || "";
+ }
+
+ private toggleIcons(icons: HTMLElement[]): void {
+ icons.forEach((icon) => {
+ icon.classList.toggle("hidden");
+ });
+ }
+
+ private showFeedback(icons: HTMLElement[]): void {
+ if (!this.btnClicked || this.isAnimationRunning === true) return;
+
+ this.isAnimationRunning = true;
+ this.toggleIcons(icons);
+ window.setTimeout(() => {
+ this.toggleIcons(icons);
+ this.isAnimationRunning = false;
+ }, Copy.FEEDBACK_DELAY);
+ }
+
+ private async copyToClipboard(codeBlock: HTMLElement, icons: HTMLElement[]): Promise {
+ const sanitizedText = this.sanitizeContent(codeBlock);
+
+ if (!navigator.clipboard) {
+ console.error("Copy: Clipboard API is not supported in this browser.");
+ this.showFeedback(icons);
+ return;
+ }
+
+ try {
+ await navigator.clipboard.writeText(sanitizedText);
+ this.showFeedback(icons);
+ } catch (err) {
+ console.error("Copy: Error while copying text.", err);
+ this.showFeedback(icons);
+ }
+ }
+}
+
+export default () => new Copy();
diff --git a/gno.land/pkg/gnoweb/frontend/js/index.ts b/gno.land/pkg/gnoweb/frontend/js/index.ts
new file mode 100644
index 00000000000..3927f794b94
--- /dev/null
+++ b/gno.land/pkg/gnoweb/frontend/js/index.ts
@@ -0,0 +1,42 @@
+(() => {
+ interface Module {
+ selector: string;
+ path: string;
+ }
+
+ const modules: Record = {
+ copy: {
+ selector: "[data-copy-btn]",
+ path: "/public/js/copy.js",
+ },
+ help: {
+ selector: "#help",
+ path: "/public/js/realmhelp.js",
+ },
+ searchBar: {
+ selector: "#header-searchbar",
+ path: "/public/js/searchbar.js",
+ },
+ };
+
+ const loadModuleIfExists = async ({ selector, path }: Module): Promise => {
+ const element = document.querySelector(selector);
+ if (element) {
+ try {
+ const module = await import(path);
+ module.default();
+ } catch (err) {
+ console.error(`Error while loading script ${path}:`, err);
+ }
+ } else {
+ console.warn(`Module not loaded: no element matches selector "${selector}"`);
+ }
+ };
+
+ const initModules = async (): Promise => {
+ const promises = Object.values(modules).map((module) => loadModuleIfExists(module));
+ await Promise.all(promises);
+ };
+
+ document.addEventListener("DOMContentLoaded", initModules);
+})();
diff --git a/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts b/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts
new file mode 100644
index 00000000000..d72102e2a2e
--- /dev/null
+++ b/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts
@@ -0,0 +1,169 @@
+import { debounce } from "./utils";
+
+class Help {
+ private DOM: {
+ el: HTMLElement | null;
+ funcs: HTMLElement[];
+ addressInput: HTMLInputElement | null;
+ cmdModeSelect: HTMLSelectElement | null;
+ };
+
+ private funcList: HelpFunc[];
+
+ private static SELECTORS = {
+ container: "#help",
+ func: "[data-func]",
+ addressInput: "[data-role='help-input-addr']",
+ cmdModeSelect: "[data-role='help-select-mode']",
+ };
+
+ constructor() {
+ this.DOM = {
+ el: document.querySelector(Help.SELECTORS.container),
+ funcs: [],
+ addressInput: null,
+ cmdModeSelect: null,
+ };
+
+ this.funcList = [];
+
+ if (this.DOM.el) {
+ this.init();
+ } else {
+ console.warn("Help: Main container not found.");
+ }
+ }
+
+ private init(): void {
+ const { el } = this.DOM;
+ if (!el) return;
+
+ this.DOM.funcs = Array.from(el.querySelectorAll(Help.SELECTORS.func));
+ this.DOM.addressInput = el.querySelector(Help.SELECTORS.addressInput);
+ this.DOM.cmdModeSelect = el.querySelector(Help.SELECTORS.cmdModeSelect);
+
+ this.funcList = this.DOM.funcs.map((funcEl) => new HelpFunc(funcEl));
+
+ this.restoreAddress();
+ this.bindEvents();
+ }
+
+ private restoreAddress(): void {
+ const { addressInput } = this.DOM;
+ if (addressInput) {
+ const storedAddress = localStorage.getItem("helpAddressInput");
+ if (storedAddress) {
+ addressInput.value = storedAddress;
+ this.funcList.forEach((func) => func.updateAddr(storedAddress));
+ }
+ }
+ }
+
+ private bindEvents(): void {
+ const { addressInput, cmdModeSelect } = this.DOM;
+
+ const debouncedUpdate = debounce((addressInput: HTMLInputElement) => {
+ const address = addressInput.value;
+
+ localStorage.setItem("helpAddressInput", address);
+ this.funcList.forEach((func) => func.updateAddr(address));
+ });
+ addressInput?.addEventListener("input", () => debouncedUpdate(addressInput));
+
+ cmdModeSelect?.addEventListener("change", (e) => {
+ const target = e.target as HTMLSelectElement;
+ this.funcList.forEach((func) => func.updateMode(target.value));
+ });
+ }
+}
+
+class HelpFunc {
+ private DOM: {
+ el: HTMLElement;
+ addrs: HTMLElement[];
+ args: HTMLElement[];
+ modes: HTMLElement[];
+ paramInputs: HTMLInputElement[];
+ };
+
+ private funcName: string | null;
+
+ private static SELECTORS = {
+ address: "[data-role='help-code-address']",
+ args: "[data-role='help-code-args']",
+ mode: "[data-code-mode]",
+ paramInput: "[data-role='help-param-input']",
+ };
+
+ constructor(el: HTMLElement) {
+ this.DOM = {
+ el,
+ addrs: Array.from(el.querySelectorAll(HelpFunc.SELECTORS.address)),
+ args: Array.from(el.querySelectorAll(HelpFunc.SELECTORS.args)),
+ modes: Array.from(el.querySelectorAll(HelpFunc.SELECTORS.mode)),
+ paramInputs: Array.from(el.querySelectorAll(HelpFunc.SELECTORS.paramInput)),
+ };
+
+ this.funcName = el.dataset.func || null;
+
+ this.initializeArgs();
+ this.bindEvents();
+ }
+
+ private static sanitizeArgsInput(input: HTMLInputElement) {
+ const paramName = input.dataset.param || "";
+ const paramValue = input.value.trim();
+
+ if (!paramName) {
+ console.warn("sanitizeArgsInput: param is missing in arg input dataset.");
+ }
+
+ return { paramName, paramValue };
+ }
+
+ private bindEvents(): void {
+ const debouncedUpdate = debounce((paramName: string, paramValue: string) => {
+ if (paramName) this.updateArg(paramName, paramValue);
+ });
+
+ this.DOM.el.addEventListener("input", (e) => {
+ const target = e.target as HTMLInputElement;
+ if (target.dataset.role === "help-param-input") {
+ const { paramName, paramValue } = HelpFunc.sanitizeArgsInput(target);
+ debouncedUpdate(paramName, paramValue);
+ }
+ });
+ }
+
+ private initializeArgs(): void {
+ this.DOM.paramInputs.forEach((input) => {
+ const { paramName, paramValue } = HelpFunc.sanitizeArgsInput(input);
+ if (paramName) this.updateArg(paramName, paramValue);
+ });
+ }
+
+ public updateArg(paramName: string, paramValue: string): void {
+ this.DOM.args
+ .filter((arg) => arg.dataset.arg === paramName)
+ .forEach((arg) => {
+ arg.textContent = paramValue || "";
+ });
+ }
+
+ public updateAddr(addr: string): void {
+ this.DOM.addrs.forEach((DOMaddr) => {
+ DOMaddr.textContent = addr.trim() || "ADDRESS";
+ });
+ }
+
+ public updateMode(mode: string): void {
+ this.DOM.modes.forEach((cmd) => {
+ const isVisible = cmd.dataset.codeMode === mode;
+ cmd.classList.toggle("inline", isVisible);
+ cmd.classList.toggle("hidden", !isVisible);
+ cmd.dataset.copyContent = isVisible ? `help-cmd-${this.funcName}` : "";
+ });
+ }
+}
+
+export default () => new Help();
diff --git a/gno.land/pkg/gnoweb/frontend/js/searchbar.ts b/gno.land/pkg/gnoweb/frontend/js/searchbar.ts
new file mode 100644
index 00000000000..6cca444aa0f
--- /dev/null
+++ b/gno.land/pkg/gnoweb/frontend/js/searchbar.ts
@@ -0,0 +1,74 @@
+class SearchBar {
+ private DOM: {
+ el: HTMLElement | null;
+ inputSearch: HTMLInputElement | null;
+ breadcrumb: HTMLElement | null;
+ };
+
+ private baseUrl: string;
+
+ private static SELECTORS = {
+ container: "#header-searchbar",
+ inputSearch: "[data-role='header-input-search']",
+ breadcrumb: "[data-role='header-breadcrumb-search']",
+ };
+
+ constructor() {
+ this.DOM = {
+ el: document.querySelector(SearchBar.SELECTORS.container),
+ inputSearch: null,
+ breadcrumb: null,
+ };
+
+ this.baseUrl = window.location.origin;
+
+ if (this.DOM.el) {
+ this.init();
+ } else {
+ console.warn("SearchBar: Main container not found.");
+ }
+ }
+
+ private init(): void {
+ const { el } = this.DOM;
+
+ this.DOM.inputSearch = el?.querySelector(SearchBar.SELECTORS.inputSearch) ?? null;
+ this.DOM.breadcrumb = el?.querySelector(SearchBar.SELECTORS.breadcrumb) ?? null;
+
+ if (!this.DOM.inputSearch) {
+ console.warn("SearchBar: Input element for search not found.");
+ }
+
+ this.bindEvents();
+ }
+
+ private bindEvents(): void {
+ this.DOM.el?.addEventListener("submit", (e) => {
+ e.preventDefault();
+ this.searchUrl();
+ });
+ }
+
+ public searchUrl(): void {
+ const input = this.DOM.inputSearch?.value.trim();
+
+ if (input) {
+ let url = input;
+
+ // Check if the URL has a proper scheme
+ if (!/^https?:\/\//i.test(url)) {
+ url = `${this.baseUrl}${url.startsWith("/") ? "" : "/"}${url}`;
+ }
+
+ try {
+ window.location.href = new URL(url).href;
+ } catch (error) {
+ console.error("SearchBar: Invalid URL. Please enter a valid URL starting with http:// or https://.");
+ }
+ } else {
+ console.error("SearchBar: Please enter a URL to search.");
+ }
+ }
+}
+
+export default () => new SearchBar();
diff --git a/gno.land/pkg/gnoweb/frontend/js/utils.ts b/gno.land/pkg/gnoweb/frontend/js/utils.ts
new file mode 100644
index 00000000000..83de509efa5
--- /dev/null
+++ b/gno.land/pkg/gnoweb/frontend/js/utils.ts
@@ -0,0 +1,12 @@
+export function debounce void>(func: T, delay: number = 250): (...args: Parameters) => void {
+ let timeoutId: ReturnType | undefined;
+
+ return function (this: any, ...args: Parameters) {
+ if (timeoutId !== undefined) {
+ clearTimeout(timeoutId);
+ }
+ timeoutId = setTimeout(() => {
+ func.apply(this, args);
+ }, delay);
+ };
+}
diff --git a/gno.land/pkg/gnoweb/static/img/favicon.ico b/gno.land/pkg/gnoweb/frontend/static/favicon.ico
similarity index 100%
rename from gno.land/pkg/gnoweb/static/img/favicon.ico
rename to gno.land/pkg/gnoweb/frontend/static/favicon.ico
diff --git a/gno.land/pkg/gnoweb/frontend/static/fonts/intervar/Intervar.woff2 b/gno.land/pkg/gnoweb/frontend/static/fonts/intervar/Intervar.woff2
new file mode 100644
index 00000000000..891fc5cc567
Binary files /dev/null and b/gno.land/pkg/gnoweb/frontend/static/fonts/intervar/Intervar.woff2 differ
diff --git a/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff b/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff
new file mode 100644
index 00000000000..2c58fe2d6d7
Binary files /dev/null and b/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff differ
diff --git a/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff2 b/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff2
new file mode 100644
index 00000000000..53d081f3a53
Binary files /dev/null and b/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff2 differ
diff --git a/gno.land/pkg/gnoweb/frontend/static/imgs/gnoland.svg b/gno.land/pkg/gnoweb/frontend/static/imgs/gnoland.svg
new file mode 100644
index 00000000000..30d2f3ef56a
--- /dev/null
+++ b/gno.land/pkg/gnoweb/frontend/static/imgs/gnoland.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/gno.land/pkg/gnoweb/gnoweb.go b/gno.land/pkg/gnoweb/gnoweb.go
deleted file mode 100644
index 5377ae6a420..00000000000
--- a/gno.land/pkg/gnoweb/gnoweb.go
+++ /dev/null
@@ -1,509 +0,0 @@
-package gnoweb
-
-import (
- "bytes"
- "embed"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "io/fs"
- "log/slog"
- "net/http"
- "net/url"
- "os"
- "path/filepath"
- "runtime"
- "strings"
- "time"
-
- "github.com/gnolang/gno/tm2/pkg/amino"
- abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
- "github.com/gnolang/gno/tm2/pkg/bft/rpc/client"
- "github.com/gnolang/gno/tm2/pkg/std"
- "github.com/gorilla/mux"
- "github.com/gotuna/gotuna"
-
- // for static files
- "github.com/gnolang/gno/gno.land/pkg/gnoweb/static"
- "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // for error types
- // "github.com/gnolang/gno/tm2/pkg/sdk" // for baseapp (info, status)
-)
-
-const (
- qFileStr = "vm/qfile"
-)
-
-//go:embed views/*
-var defaultViewsFiles embed.FS
-
-type Config struct {
- RemoteAddr string
- CaptchaSite string
- FaucetURL string
- ViewsDir string
- HelpChainID string
- HelpRemote string
- WithAnalytics bool
-}
-
-func NewDefaultConfig() Config {
- return Config{
- RemoteAddr: "127.0.0.1:26657",
- CaptchaSite: "",
- FaucetURL: "http://localhost:5050",
- ViewsDir: "",
- HelpChainID: "dev",
- HelpRemote: "127.0.0.1:26657",
- WithAnalytics: false,
- }
-}
-
-func MakeApp(logger *slog.Logger, cfg Config) gotuna.App {
- var viewFiles fs.FS
-
- // Get specific views directory if specified
- if cfg.ViewsDir != "" {
- viewFiles = os.DirFS(cfg.ViewsDir)
- } else {
- // Get embed views
- var err error
- viewFiles, err = fs.Sub(defaultViewsFiles, "views")
- if err != nil {
- panic("unable to get views directory from embed fs: " + err.Error())
- }
- }
-
- app := gotuna.App{
- ViewFiles: viewFiles,
- Router: gotuna.NewMuxRouter(),
- Static: static.EmbeddedStatic,
- }
-
- for from, to := range Aliases {
- app.Router.Handle(from, handlerRealmAlias(logger, app, &cfg, to))
- }
-
- for from, to := range Redirects {
- app.Router.Handle(from, handlerRedirect(logger, app, &cfg, to))
- }
- // realm routes
- // NOTE: see rePathPart.
- app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}/{filename:(?:(?:.*\\.(?:gno|md|txt|mod)$)|(?:LICENSE$))?}", handlerRealmFile(logger, app, &cfg))
- app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}", handlerRealmMain(logger, app, &cfg))
- app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}:{querystr:.*}", handlerRealmRender(logger, app, &cfg))
- app.Router.Handle("/p/{filepath:.*}", handlerPackageFile(logger, app, &cfg))
-
- // other
- app.Router.Handle("/faucet", handlerFaucet(logger, app, &cfg))
- app.Router.Handle("/static/{path:.+}", handlerStaticFile(logger, app, &cfg))
- app.Router.Handle("/favicon.ico", handlerFavicon(logger, app, &cfg))
-
- // api
- app.Router.Handle("/status.json", handlerStatusJSON(logger, app, &cfg))
-
- app.Router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- path := r.RequestURI
- handleNotFound(logger, app, &cfg, path, w, r)
- })
- return app
-}
-
-// handlerRealmAlias is used to render official pages from realms.
-// url is intended to be shorter.
-// UX is intended to be more minimalistic.
-// A link to the realm realm is added.
-func handlerRealmAlias(logger *slog.Logger, app gotuna.App, cfg *Config, rlmpath string) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- rlmfullpath := "gno.land" + rlmpath
- querystr := "" // XXX: "?gnoweb-alias=1"
- parts := strings.Split(rlmpath, ":")
- switch len(parts) {
- case 1: // continue
- case 2: // r/realm:querystr
- rlmfullpath = "gno.land" + parts[0]
- querystr = parts[1] + querystr
- default:
- panic("should not happen")
- }
- rlmname := strings.TrimPrefix(rlmfullpath, "gno.land/r/")
- qpath := "vm/qrender"
- data := []byte(fmt.Sprintf("%s:%s", rlmfullpath, querystr))
- res, err := makeRequest(logger, cfg, qpath, data)
- if err != nil {
- writeError(logger, w, fmt.Errorf("gnoweb failed to query gnoland: %w", err))
- return
- }
-
- queryParts := strings.Split(querystr, "/")
- pathLinks := []pathLink{}
- for i, part := range queryParts {
- pathLinks = append(pathLinks, pathLink{
- URL: "/r/" + rlmname + ":" + strings.Join(queryParts[:i+1], "/"),
- Text: part,
- })
- }
-
- tmpl := app.NewTemplatingEngine()
- // XXX: extract title from realm's output
- // XXX: extract description from realm's output
- tmpl.Set("RealmName", rlmname)
- tmpl.Set("RealmPath", rlmpath)
- tmpl.Set("Query", querystr)
- tmpl.Set("PathLinks", pathLinks)
- tmpl.Set("Contents", string(res.Data))
- tmpl.Set("Config", cfg)
- tmpl.Set("IsAlias", true)
- tmpl.Render(w, r, "realm_render.html", "funcs.html")
- })
-}
-
-func handlerFaucet(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- app.NewTemplatingEngine().
- Set("Config", cfg).
- Render(w, r, "faucet.html", "funcs.html")
- })
-}
-
-func handlerStatusJSON(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler {
- startedAt := time.Now()
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- var ret struct {
- Gnoland struct {
- Connected bool `json:"connected"`
- Error *string `json:"error,omitempty"`
- Height *int64 `json:"height,omitempty"`
- // processed txs
- // active connections
-
- Version *string `json:"version,omitempty"`
- // Uptime *float64 `json:"uptime-seconds,omitempty"`
- // Goarch *string `json:"goarch,omitempty"`
- // Goos *string `json:"goos,omitempty"`
- // GoVersion *string `json:"go-version,omitempty"`
- // NumCPU *int `json:"num_cpu,omitempty"`
- } `json:"gnoland"`
- Website struct {
- // Version string `json:"version"`
- Uptime float64 `json:"uptime-seconds"`
- Goarch string `json:"goarch"`
- Goos string `json:"goos"`
- GoVersion string `json:"go-version"`
- NumCPU int `json:"num_cpu"`
- } `json:"website"`
- }
- ret.Website.Uptime = time.Since(startedAt).Seconds()
- ret.Website.Goarch = runtime.GOARCH
- ret.Website.Goos = runtime.GOOS
- ret.Website.NumCPU = runtime.NumCPU()
- ret.Website.GoVersion = runtime.Version()
-
- ret.Gnoland.Connected = true
- res, err := makeRequest(logger, cfg, ".app/version", []byte{})
- if err != nil {
- ret.Gnoland.Connected = false
- errmsg := err.Error()
- ret.Gnoland.Error = &errmsg
- } else {
- version := string(res.Value)
- ret.Gnoland.Version = &version
- ret.Gnoland.Height = &res.Height
- }
-
- out, _ := json.MarshalIndent(ret, "", " ")
- w.Header().Set("Content-Type", "application/json")
- w.Write(out)
- })
-}
-
-func handlerRedirect(logger *slog.Logger, app gotuna.App, cfg *Config, to string) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- http.Redirect(w, r, to, http.StatusFound)
- tmpl := app.NewTemplatingEngine()
- tmpl.Set("To", to)
- tmpl.Set("Config", cfg)
- tmpl.Render(w, r, "redirect.html", "funcs.html")
- })
-}
-
-func handlerRealmMain(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- rlmname := vars["rlmname"]
- rlmpath := "gno.land/r/" + rlmname
- query := r.URL.Query()
-
- logger.Info("handling", "name", rlmname, "path", rlmpath)
- if query.Has("help") {
- // Render function helper.
- funcName := query.Get("__func")
- qpath := "vm/qfuncs"
- data := []byte(rlmpath)
- res, err := makeRequest(logger, cfg, qpath, data)
- if err != nil {
- writeError(logger, w, fmt.Errorf("request failed: %w", err))
- return
- }
- var fsigs vm.FunctionSignatures
- amino.MustUnmarshalJSON(res.Data, &fsigs)
- // Fill fsigs with query parameters.
- for i := range fsigs {
- fsig := &(fsigs[i])
- for j := range fsig.Params {
- param := &(fsig.Params[j])
- value := query.Get(param.Name)
- param.Value = value
- }
- }
- // Render template.
- tmpl := app.NewTemplatingEngine()
- tmpl.Set("FuncName", funcName)
- tmpl.Set("RealmPath", rlmpath)
- tmpl.Set("DirPath", pathOf(rlmpath))
- tmpl.Set("FunctionSignatures", fsigs)
- tmpl.Set("Config", cfg)
- tmpl.Render(w, r, "realm_help.html", "funcs.html")
- } else {
- // Ensure realm exists. TODO optimize.
- qpath := qFileStr
- data := []byte(rlmpath)
- _, err := makeRequest(logger, cfg, qpath, data)
- if err != nil {
- writeError(logger, w, errors.New("error querying realm package"))
- return
- }
- // Render blank query path, /r/REALM:.
- handleRealmRender(logger, app, cfg, w, r)
- }
- })
-}
-
-type pathLink struct {
- URL string
- Text string
-}
-
-func handlerRealmRender(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- handleRealmRender(logger, app, cfg, w, r)
- })
-}
-
-func handleRealmRender(logger *slog.Logger, app gotuna.App, cfg *Config, w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- rlmname := vars["rlmname"]
- rlmpath := "gno.land/r/" + rlmname
- querystr := vars["querystr"]
- if r.URL.Path == "/r/"+rlmname+":" {
- // Redirect to /r/REALM if querypath is empty.
- http.Redirect(w, r, "/r/"+rlmname, http.StatusFound)
- return
- }
- qpath := "vm/qrender"
- data := []byte(fmt.Sprintf("%s:%s", rlmpath, querystr))
- res, err := makeRequest(logger, cfg, qpath, data)
- if err != nil {
- // XXX hack
- if strings.Contains(err.Error(), "Render not declared") {
- res = &abci.ResponseQuery{}
- res.Data = []byte("realm package has no Render() function")
- } else {
- writeError(logger, w, err)
- return
- }
- }
-
- dirdata := []byte(rlmpath)
- dirres, err := makeRequest(logger, cfg, qFileStr, dirdata)
- if err != nil {
- writeError(logger, w, err)
- return
- }
- hasReadme := bytes.Contains(append(dirres.Data, '\n'), []byte("README.md\n"))
-
- // linkify querystr.
- queryParts := strings.Split(querystr, "/")
- pathLinks := []pathLink{}
- for i, part := range queryParts {
- pathLinks = append(pathLinks, pathLink{
- URL: "/r/" + rlmname + ":" + strings.Join(queryParts[:i+1], "/"),
- Text: part,
- })
- }
- // Render template.
- tmpl := app.NewTemplatingEngine()
- // XXX: extract title from realm's output
- // XXX: extract description from realm's output
- tmpl.Set("RealmName", rlmname)
- tmpl.Set("RealmPath", rlmpath)
- tmpl.Set("Query", querystr)
- tmpl.Set("PathLinks", pathLinks)
- tmpl.Set("Contents", string(res.Data))
- tmpl.Set("Config", cfg)
- tmpl.Set("HasReadme", hasReadme)
- tmpl.Render(w, r, "realm_render.html", "funcs.html")
-}
-
-func handlerRealmFile(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- diruri := "gno.land/r/" + vars["rlmname"]
- filename := vars["filename"]
- renderPackageFile(logger, app, cfg, w, r, diruri, filename)
- })
-}
-
-func handlerPackageFile(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- pkgpath := "gno.land/p/" + vars["filepath"]
- diruri, filename := std.SplitFilepath(pkgpath)
- if filename == "" && diruri == pkgpath {
- // redirect to diruri + "/"
- http.Redirect(w, r, "/p/"+vars["filepath"]+"/", http.StatusFound)
- return
- }
- renderPackageFile(logger, app, cfg, w, r, diruri, filename)
- })
-}
-
-func renderPackageFile(logger *slog.Logger, app gotuna.App, cfg *Config, w http.ResponseWriter, r *http.Request, diruri string, filename string) {
- if filename == "" {
- // Request is for a folder.
- qpath := qFileStr
- data := []byte(diruri)
- res, err := makeRequest(logger, cfg, qpath, data)
- if err != nil {
- writeError(logger, w, err)
- return
- }
- files := strings.Split(string(res.Data), "\n")
- // Render template.
- tmpl := app.NewTemplatingEngine()
- tmpl.Set("DirURI", diruri)
- tmpl.Set("DirPath", pathOf(diruri))
- tmpl.Set("Files", files)
- tmpl.Set("Config", cfg)
- tmpl.Render(w, r, "package_dir.html", "funcs.html")
- } else {
- // Request is for a file.
- filepath := diruri + "/" + filename
- qpath := qFileStr
- data := []byte(filepath)
- res, err := makeRequest(logger, cfg, qpath, data)
- if err != nil {
- writeError(logger, w, err)
- return
- }
- // Render template.
- tmpl := app.NewTemplatingEngine()
- tmpl.Set("DirURI", diruri)
- tmpl.Set("DirPath", pathOf(diruri))
- tmpl.Set("FileName", filename)
- tmpl.Set("FileContents", string(res.Data))
- tmpl.Set("Config", cfg)
- tmpl.Render(w, r, "package_file.html", "funcs.html")
- }
-}
-
-func makeRequest(log *slog.Logger, cfg *Config, qpath string, data []byte) (res *abci.ResponseQuery, err error) {
- opts2 := client.ABCIQueryOptions{
- // Height: height, XXX
- // Prove: false, XXX
- }
- remote := cfg.RemoteAddr
- cli, err := client.NewHTTPClient(remote)
- if err != nil {
- return nil, fmt.Errorf("unable to create HTTP client, %w", err)
- }
-
- qres, err := cli.ABCIQueryWithOptions(
- qpath, data, opts2)
- if err != nil {
- log.Error("request error", "path", qpath, "error", err)
- return nil, fmt.Errorf("unable to query path %q: %w", qpath, err)
- }
- if qres.Response.Error != nil {
- log.Error("response error", "path", qpath, "log", qres.Response.Log)
- return nil, qres.Response.Error
- }
- return &qres.Response, nil
-}
-
-func handlerStaticFile(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler {
- fs := http.FS(app.Static)
- fileapp := http.StripPrefix("/static", http.FileServer(fs))
-
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- fpath := filepath.Clean(vars["path"])
- f, err := fs.Open(fpath)
- if os.IsNotExist(err) {
- handleNotFound(logger, app, cfg, fpath, w, r)
- return
- }
- stat, err := f.Stat()
- if err != nil || stat.IsDir() {
- handleNotFound(logger, app, cfg, fpath, w, r)
- return
- }
-
- // TODO: ModTime doesn't work for embed?
- // w.Header().Set("ETag", fmt.Sprintf("%x", stat.ModTime().UnixNano()))
- // w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%s", "31536000"))
- fileapp.ServeHTTP(w, r)
- })
-}
-
-func handlerFavicon(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler {
- fs := http.FS(app.Static)
-
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- fpath := "img/favicon.ico"
- f, err := fs.Open(fpath)
- if os.IsNotExist(err) {
- handleNotFound(logger, app, cfg, fpath, w, r)
- return
- }
- w.Header().Set("Content-Type", "image/x-icon")
- w.Header().Set("Cache-Control", "public, max-age=604800") // 7d
- io.Copy(w, f)
- })
-}
-
-func handleNotFound(logger *slog.Logger, app gotuna.App, cfg *Config, path string, w http.ResponseWriter, r *http.Request) {
- // decode path for non-ascii characters
- decodedPath, err := url.PathUnescape(path)
- if err != nil {
- logger.Error("failed to decode path", err)
- decodedPath = path
- }
- w.WriteHeader(http.StatusNotFound)
- app.NewTemplatingEngine().
- Set("title", "Not found").
- Set("path", decodedPath).
- Set("Config", cfg).
- Render(w, r, "404.html", "funcs.html")
-}
-
-func writeError(logger *slog.Logger, w http.ResponseWriter, err error) {
- if details := errors.Unwrap(err); details != nil {
- logger.Error("handler", "error", err, "details", details)
- } else {
- logger.Error("handler", "error:", err)
- }
-
- // XXX: writeError should return an error page template.
- w.WriteHeader(500)
- w.Write([]byte(err.Error()))
-}
-
-func pathOf(diruri string) string {
- parts := strings.Split(diruri, "/")
- if parts[0] == "gno.land" {
- return "/" + strings.Join(parts[1:], "/")
- }
-
- panic(fmt.Sprintf("invalid dir-URI %q", diruri))
-}
diff --git a/gno.land/pkg/gnoweb/gnoweb_test.go b/gno.land/pkg/gnoweb/gnoweb_test.go
deleted file mode 100644
index b266dc80a6a..00000000000
--- a/gno.land/pkg/gnoweb/gnoweb_test.go
+++ /dev/null
@@ -1,138 +0,0 @@
-package gnoweb
-
-import (
- "fmt"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
-
- "github.com/gnolang/gno/gno.land/pkg/integration"
- "github.com/gnolang/gno/gnovm/pkg/gnoenv"
- "github.com/gnolang/gno/tm2/pkg/log"
- "github.com/gotuna/gotuna/test/assert"
-)
-
-func TestRoutes(t *testing.T) {
- const (
- ok = http.StatusOK
- found = http.StatusFound
- notFound = http.StatusNotFound
- )
- routes := []struct {
- route string
- status int
- substring string
- }{
- {"/", ok, "Welcome"}, // assert / gives 200 (OK). assert / contains "Welcome".
- {"/about", ok, "blockchain"},
- {"/r/gnoland/blog", ok, ""}, // whatever content
- {"/r/gnoland/blog?help", ok, "exposed"},
- {"/r/gnoland/blog/", ok, "admin.gno"},
- {"/r/gnoland/blog/admin.gno", ok, "func "},
- {"/r/demo/users:administrator", ok, "address"},
- {"/r/demo/users", ok, "manfred"},
- {"/r/demo/users/users.gno", ok, "// State"},
- {"/r/demo/deep/very/deep", ok, "it works!"},
- {"/r/demo/deep/very/deep:bob", ok, "hi bob"},
- {"/r/demo/deep/very/deep?help", ok, "exposed"},
- {"/r/demo/deep/very/deep/", ok, "render.gno"},
- {"/r/demo/deep/very/deep/render.gno", ok, "func Render("},
- {"/game-of-realms", ok, "/r/gnoland/pages:p/gor"},
- {"/gor", found, "/game-of-realms"},
- {"/blog", found, "/r/gnoland/blog"},
- {"/404-not-found", notFound, "/404-not-found"},
- {"/아스키문자가아닌경로", notFound, "/아스키문자가아닌경로"},
- {"/%ED%85%8C%EC%8A%A4%ED%8A%B8", notFound, "/테스트"},
- {"/グノー", notFound, "/グノー"},
- {"/⚛️", notFound, "/⚛️"},
- {"/p/demo/flow/LICENSE", ok, "BSD 3-Clause"},
- }
-
- 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()
-
- cfg := NewDefaultConfig()
-
- logger := log.NewTestingLogger(t)
-
- // set the `remoteAddr` of the client to the listening address of the
- // node, which is randomly assigned.
- cfg.RemoteAddr = remoteAddr
- app := MakeApp(logger, cfg)
-
- for _, r := range routes {
- t.Run(fmt.Sprintf("test route %s", r.route), func(t *testing.T) {
- request := httptest.NewRequest(http.MethodGet, r.route, nil)
- response := httptest.NewRecorder()
- app.Router.ServeHTTP(response, request)
- assert.Equal(t, r.status, response.Code)
- assert.Contains(t, response.Body.String(), r.substring)
- })
- }
-}
-
-func TestAnalytics(t *testing.T) {
- routes := []string{
- // special realms
- "/", // home
- "/about",
- "/start",
-
- // redirects
- "/game-of-realms",
- "/getting-started",
- "/blog",
- "/boards",
-
- // realm, source, help page
- "/r/gnoland/blog",
- "/r/gnoland/blog/admin.gno",
- "/r/demo/users:administrator",
- "/r/gnoland/blog?help",
-
- // special pages
- "/404-not-found",
- }
-
- 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()
-
- cfg := NewDefaultConfig()
- cfg.RemoteAddr = remoteAddr
-
- logger := log.NewTestingLogger(t)
-
- t.Run("with", func(t *testing.T) {
- for _, route := range routes {
- t.Run(route, func(t *testing.T) {
- ccfg := cfg // clone config
- ccfg.WithAnalytics = true
- app := MakeApp(logger, ccfg)
- request := httptest.NewRequest(http.MethodGet, route, nil)
- response := httptest.NewRecorder()
- app.Router.ServeHTTP(response, request)
- assert.Contains(t, response.Body.String(), "sa.gno.services")
- })
- }
- })
- t.Run("without", func(t *testing.T) {
- for _, route := range routes {
- t.Run(route, func(t *testing.T) {
- ccfg := cfg // clone config
- ccfg.WithAnalytics = false
- app := MakeApp(logger, ccfg)
- request := httptest.NewRequest(http.MethodGet, route, nil)
- response := httptest.NewRecorder()
- app.Router.ServeHTTP(response, request)
- assert.Equal(t, strings.Contains(response.Body.String(), "sa.gno.services"), false)
- })
- }
- })
-}
diff --git a/gno.land/pkg/gnoweb/handler.go b/gno.land/pkg/gnoweb/handler.go
new file mode 100644
index 00000000000..2dc51d64029
--- /dev/null
+++ b/gno.land/pkg/gnoweb/handler.go
@@ -0,0 +1,347 @@
+package gnoweb
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "html/template"
+ "io"
+ "log/slog"
+ "net/http"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/gnolang/gno/gno.land/pkg/gnoweb/components"
+ "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // For error types
+)
+
+// StaticMetadata holds static configuration for a web handler.
+type StaticMetadata struct {
+ Domain string
+ AssetsPath string
+ ChromaPath string
+ RemoteHelp string
+ ChainId string
+ Analytics bool
+}
+
+// WebHandlerConfig configures a WebHandler.
+type WebHandlerConfig struct {
+ Meta StaticMetadata
+ WebClient WebClient
+}
+
+// validate checks if the WebHandlerConfig is valid.
+func (cfg WebHandlerConfig) validate() error {
+ if cfg.WebClient == nil {
+ return errors.New("no `WebClient` configured")
+ }
+ return nil
+}
+
+// WebHandler processes HTTP requests.
+type WebHandler struct {
+ Logger *slog.Logger
+ Static StaticMetadata
+ Client WebClient
+}
+
+// NewWebHandler creates a new WebHandler.
+func NewWebHandler(logger *slog.Logger, cfg WebHandlerConfig) (*WebHandler, error) {
+ if err := cfg.validate(); err != nil {
+ return nil, fmt.Errorf("config validate error: %w", err)
+ }
+
+ return &WebHandler{
+ Client: cfg.WebClient,
+ Static: cfg.Meta,
+ Logger: logger,
+ }, nil
+}
+
+// ServeHTTP handles HTTP requests.
+func (h *WebHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ h.Logger.Debug("receiving request", "method", r.Method, "path", r.URL.Path)
+
+ if r.Method != http.MethodGet {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ h.Get(w, r)
+}
+
+// Get processes a GET HTTP request.
+func (h *WebHandler) Get(w http.ResponseWriter, r *http.Request) {
+ var body bytes.Buffer
+
+ start := time.Now()
+ defer func() {
+ h.Logger.Debug("request completed",
+ "url", r.URL.String(),
+ "elapsed", time.Since(start).String())
+ }()
+
+ indexData := components.IndexData{
+ HeadData: components.HeadData{
+ AssetsPath: h.Static.AssetsPath,
+ ChromaPath: h.Static.ChromaPath,
+ },
+ FooterData: components.FooterData{
+ Analytics: h.Static.Analytics,
+ AssetsPath: h.Static.AssetsPath,
+ },
+ }
+
+ status, err := h.renderPage(&body, r, &indexData)
+ if err != nil {
+ http.Error(w, "internal server error", http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(status)
+
+ // NOTE: HTML escaping should have already been done by markdown rendering package
+ indexData.Body = template.HTML(body.String()) //nolint:gosec
+
+ // Render the final page with the rendered body
+ if err = components.RenderIndexComponent(w, indexData); err != nil {
+ h.Logger.Error("failed to render index component", "err", err)
+ }
+}
+
+// renderPage renders the page into the given buffer and prepares the index data.
+func (h *WebHandler) renderPage(body *bytes.Buffer, r *http.Request, indexData *components.IndexData) (int, error) {
+ gnourl, err := ParseGnoURL(r.URL)
+ if err != nil {
+ h.Logger.Warn("unable to parse url path", "path", r.URL.Path, "err", err)
+ return http.StatusNotFound, components.RenderStatusComponent(body, "invalid path")
+ }
+
+ breadcrumb := generateBreadcrumbPaths(gnourl)
+ indexData.HeadData.Title = h.Static.Domain + " - " + gnourl.Path
+ indexData.HeaderData = components.HeaderData{
+ RealmPath: gnourl.Encode(EncodePath | EncodeArgs | EncodeQuery | EncodeNoEscape),
+ Breadcrumb: breadcrumb,
+ WebQuery: gnourl.WebQuery,
+ }
+
+ switch {
+ case gnourl.IsRealm(), gnourl.IsPure():
+ return h.GetPackagePage(body, gnourl)
+ default:
+ h.Logger.Debug("invalid path: path is neither a pure package or a realm")
+ return http.StatusBadRequest, components.RenderStatusComponent(body, "invalid path")
+ }
+}
+
+// GetPackagePage handles package pages.
+func (h *WebHandler) GetPackagePage(w io.Writer, gnourl *GnoURL) (int, error) {
+ h.Logger.Info("component render", "path", gnourl.Path, "args", gnourl.Args)
+
+ // Handle Help page
+ if gnourl.WebQuery.Has("help") {
+ return h.GetHelpPage(w, gnourl)
+ }
+
+ // Handle Source page
+ if gnourl.WebQuery.Has("source") || gnourl.IsFile() {
+ return h.GetSourcePage(w, gnourl)
+ }
+
+ // Handle Source page
+ if gnourl.IsDir() || gnourl.IsPure() {
+ return h.GetDirectoryPage(w, gnourl)
+ }
+
+ // Ultimately render realm content
+ return h.renderRealmContent(w, gnourl)
+}
+
+// renderRealmContent renders the content of a realm.
+func (h *WebHandler) renderRealmContent(w io.Writer, gnourl *GnoURL) (int, error) {
+ var content bytes.Buffer
+ meta, err := h.Client.RenderRealm(&content, gnourl.Path, gnourl.EncodeArgs())
+ if err != nil {
+ h.Logger.Error("unable to render realm", "err", err, "path", gnourl.EncodeArgs())
+ return renderClientErrorStatusPage(w, gnourl, err)
+ }
+
+ err = components.RenderRealmComponent(w, components.RealmData{
+ TocItems: &components.RealmTOCData{
+ Items: meta.Toc.Items,
+ },
+ // NOTE: `RenderRealm` should ensure that HTML content is
+ // sanitized before rendering
+ Content: template.HTML(content.String()), //nolint:gosec
+ })
+ if err != nil {
+ h.Logger.Error("unable to render template", "err", err)
+ return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error")
+ }
+
+ return http.StatusOK, nil
+}
+
+// GetHelpPage renders the help page.
+func (h *WebHandler) GetHelpPage(w io.Writer, gnourl *GnoURL) (int, error) {
+ fsigs, err := h.Client.Functions(gnourl.Path)
+ if err != nil {
+ h.Logger.Error("unable to fetch path functions", "err", err)
+ return renderClientErrorStatusPage(w, gnourl, err)
+ }
+
+ selArgs := make(map[string]string)
+ selFn := gnourl.WebQuery.Get("func")
+ if selFn != "" {
+ for _, fn := range fsigs {
+ if selFn != fn.FuncName {
+ continue
+ }
+
+ for _, param := range fn.Params {
+ selArgs[param.Name] = gnourl.WebQuery.Get(param.Name)
+ }
+
+ fsigs = []vm.FunctionSignature{fn}
+ break
+ }
+ }
+
+ realmName := filepath.Base(gnourl.Path)
+ err = components.RenderHelpComponent(w, components.HelpData{
+ SelectedFunc: selFn,
+ SelectedArgs: selArgs,
+ RealmName: realmName,
+ ChainId: h.Static.ChainId,
+ // TODO: get chain domain and use that.
+ PkgPath: filepath.Join(h.Static.Domain, gnourl.Path),
+ Remote: h.Static.RemoteHelp,
+ Functions: fsigs,
+ })
+ if err != nil {
+ h.Logger.Error("unable to render helper", "err", err)
+ return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error")
+ }
+
+ return http.StatusOK, nil
+}
+
+// GetSource renders the source page.
+func (h *WebHandler) GetSourcePage(w io.Writer, gnourl *GnoURL) (int, error) {
+ pkgPath := gnourl.Path
+ files, err := h.Client.Sources(pkgPath)
+ if err != nil {
+ h.Logger.Error("unable to list sources file", "path", gnourl.Path, "err", err)
+ return renderClientErrorStatusPage(w, gnourl, err)
+ }
+
+ if len(files) == 0 {
+ h.Logger.Debug("no files available", "path", gnourl.Path)
+ return http.StatusOK, components.RenderStatusComponent(w, "no files available")
+ }
+
+ var fileName string
+ if gnourl.IsFile() { // check path file from path first
+ fileName = gnourl.File
+ } else if file := gnourl.WebQuery.Get("file"); file != "" {
+ fileName = file
+ }
+
+ if fileName == "" {
+ fileName = files[0] // fallback on the first file if
+ }
+
+ var source bytes.Buffer
+ meta, err := h.Client.SourceFile(&source, pkgPath, fileName)
+ if err != nil {
+ h.Logger.Error("unable to get source file", "file", fileName, "err", err)
+ return renderClientErrorStatusPage(w, gnourl, err)
+ }
+
+ fileSizeStr := fmt.Sprintf("%.2f Kb", meta.SizeKb)
+ err = components.RenderSourceComponent(w, components.SourceData{
+ PkgPath: gnourl.Path,
+ Files: files,
+ FileName: fileName,
+ FileCounter: len(files),
+ FileLines: meta.Lines,
+ FileSize: fileSizeStr,
+ FileSource: template.HTML(source.String()), //nolint:gosec
+ })
+ if err != nil {
+ h.Logger.Error("unable to render helper", "err", err)
+ return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error")
+ }
+
+ return http.StatusOK, nil
+}
+
+// GetDirectoryPage renders the directory page.
+func (h *WebHandler) GetDirectoryPage(w io.Writer, gnourl *GnoURL) (int, error) {
+ pkgPath := strings.TrimSuffix(gnourl.Path, "/")
+
+ files, err := h.Client.Sources(pkgPath)
+ if err != nil {
+ h.Logger.Error("unable to list sources file", "path", gnourl.Path, "err", err)
+ return renderClientErrorStatusPage(w, gnourl, err)
+ }
+
+ if len(files) == 0 {
+ h.Logger.Debug("no files available", "path", gnourl.Path)
+ return http.StatusOK, components.RenderStatusComponent(w, "no files available")
+ }
+
+ err = components.RenderDirectoryComponent(w, components.DirData{
+ PkgPath: gnourl.Path,
+ Files: files,
+ FileCounter: len(files),
+ })
+ if err != nil {
+ h.Logger.Error("unable to render directory", "err", err)
+ return http.StatusInternalServerError, components.RenderStatusComponent(w, "not found")
+ }
+
+ return http.StatusOK, nil
+}
+
+func renderClientErrorStatusPage(w io.Writer, _ *GnoURL, err error) (int, error) {
+ if err == nil {
+ return http.StatusOK, nil
+ }
+
+ switch {
+ case errors.Is(err, ErrClientPathNotFound):
+ return http.StatusNotFound, components.RenderStatusComponent(w, err.Error())
+ case errors.Is(err, ErrClientBadRequest):
+ return http.StatusInternalServerError, components.RenderStatusComponent(w, "bad request")
+ case errors.Is(err, ErrClientResponse):
+ fallthrough // XXX: for now fallback as internal error
+ default:
+ return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error")
+ }
+}
+
+func generateBreadcrumbPaths(url *GnoURL) components.BreadcrumbData {
+ split := strings.Split(url.Path, "/")
+
+ var data components.BreadcrumbData
+ var name string
+ for i := range split {
+ if name = split[i]; name == "" {
+ continue
+ }
+
+ data.Parts = append(data.Parts, components.BreadcrumbPart{
+ Name: name,
+ URL: strings.Join(split[:i+1], "/"),
+ })
+ }
+
+ if args := url.EncodeArgs(); args != "" {
+ data.Args = args
+ }
+
+ return data
+}
diff --git a/gno.land/pkg/gnoweb/handler_test.go b/gno.land/pkg/gnoweb/handler_test.go
new file mode 100644
index 00000000000..624e3390a97
--- /dev/null
+++ b/gno.land/pkg/gnoweb/handler_test.go
@@ -0,0 +1,112 @@
+package gnoweb_test
+
+import (
+ "log/slog"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/gnolang/gno/gno.land/pkg/gnoweb"
+ "github.com/gnolang/gno/gno.land/pkg/sdk/vm"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testingLogger struct {
+ *testing.T
+}
+
+func (t *testingLogger) Write(b []byte) (n int, err error) {
+ t.T.Log(strings.TrimSpace(string(b)))
+ return len(b), nil
+}
+
+// TestWebHandler_Get tests the Get method of WebHandler using table-driven tests.
+func TestWebHandler_Get(t *testing.T) {
+ // Set up a mock package with some files and functions
+ mockPackage := &gnoweb.MockPackage{
+ Domain: "example.com",
+ Path: "/r/mock/path",
+ Files: map[string]string{
+ "render.gno": `package main; func Render(path string) { return "one more time" }`,
+ "gno.mod": `module example.com/r/mock/path`,
+ "LicEnse": `my super license`,
+ },
+ Functions: []vm.FunctionSignature{
+ {FuncName: "SuperRenderFunction", Params: []vm.NamedType{
+ {Name: "my_super_arg", Type: "string"},
+ }},
+ },
+ }
+
+ // Create a mock web client with the mock package
+ webclient := gnoweb.NewMockWebClient(mockPackage)
+
+ // Create a WebHandlerConfig with the mock web client and static metadata
+ config := gnoweb.WebHandlerConfig{
+ WebClient: webclient,
+ }
+
+ // Define test cases
+ cases := []struct {
+ Path string
+ Status int
+ Contain string // optional
+ Contains []string // optional
+ }{
+ // Found
+ {Path: "/r/mock/path", Status: http.StatusOK, Contain: "[example.com]/r/mock/path"},
+
+ // Source page
+ {Path: "/r/mock/path/", Status: http.StatusOK, Contain: "Directory"},
+ {Path: "/r/mock/path/render.gno", Status: http.StatusOK, Contain: "one more time"},
+ {Path: "/r/mock/path/LicEnse", Status: http.StatusOK, Contain: "my super license"},
+ {Path: "/r/mock/path$source&file=render.gno", Status: http.StatusOK, Contain: "one more time"},
+ {Path: "/r/mock/path$source", Status: http.StatusOK, Contain: "module"}, // `gno.mod` by default
+ {Path: "/r/mock/path/license", Status: http.StatusNotFound},
+
+ // Help page
+ {Path: "/r/mock/path$help", Status: http.StatusOK, Contains: []string{
+ "my_super_arg",
+ "SuperRenderFunction",
+ }},
+
+ // Package not found
+ {Path: "/r/invalid/path", Status: http.StatusNotFound, Contain: "not found"},
+
+ // Invalid path
+ {Path: "/r", Status: http.StatusBadRequest, Contain: "invalid path"},
+ {Path: "/r/~!1337", Status: http.StatusNotFound, Contain: "invalid path"},
+ }
+
+ for _, tc := range cases {
+ t.Run(strings.TrimPrefix(tc.Path, "/"), func(t *testing.T) {
+ t.Logf("input: %+v", tc)
+
+ // Initialize testing logger
+ logger := slog.New(slog.NewTextHandler(&testingLogger{t}, &slog.HandlerOptions{}))
+
+ // Create a new WebHandler
+ handler, err := gnoweb.NewWebHandler(logger, config)
+ require.NoError(t, err)
+
+ // Create a new HTTP request for each test case
+ req, err := http.NewRequest(http.MethodGet, tc.Path, nil)
+ require.NoError(t, err)
+
+ // Create a ResponseRecorder to capture the response
+ rr := httptest.NewRecorder()
+
+ // Invoke serve method
+ handler.ServeHTTP(rr, req)
+
+ // Assert result
+ assert.Equal(t, tc.Status, rr.Code)
+ assert.Containsf(t, rr.Body.String(), tc.Contain, "rendered body should contain: %q", tc.Contain)
+ for _, contain := range tc.Contains {
+ assert.Containsf(t, rr.Body.String(), contain, "rendered body should contain: %q", contain)
+ }
+ })
+ }
+}
diff --git a/gno.land/pkg/gnoweb/markdown/toc.go b/gno.land/pkg/gnoweb/markdown/toc.go
new file mode 100644
index 00000000000..ceafbd7cc96
--- /dev/null
+++ b/gno.land/pkg/gnoweb/markdown/toc.go
@@ -0,0 +1,137 @@
+// This file is a minimal version of https://github.com/abhinav/goldmark-toc
+
+package markdown
+
+import (
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/util"
+)
+
+const MaxDepth = 6
+
+type Toc struct {
+ Items []*TocItem
+}
+
+type TocItem struct {
+ // Title of this item in the table of contents.
+ //
+ // This may be blank for items that don't refer to a heading, and only
+ // have sub-items.
+ Title []byte
+
+ // ID is the identifier for the heading that this item refers to. This
+ // is the fragment portion of the link without the "#".
+ //
+ // This may be blank if the item doesn't have an id assigned to it, or
+ // if it doesn't have a title.
+ //
+ // Enable AutoHeadingID in your parser if you expected these to be set
+ // but they weren't.
+ ID []byte
+
+ // Items references children of this item.
+ //
+ // For a heading at level 3, Items, contains the headings at level 4
+ // under that section.
+ Items []*TocItem
+}
+
+func (i TocItem) Anchor() string {
+ return "#" + string(i.ID)
+}
+
+type TocOptions struct {
+ MinDepth, MaxDepth int
+}
+
+func TocInspect(n ast.Node, src []byte, opts TocOptions) (Toc, error) {
+ // Appends an empty subitem to the given node
+ // and returns a reference to it.
+ appendChild := func(n *TocItem) *TocItem {
+ child := new(TocItem)
+ n.Items = append(n.Items, child)
+ return child
+ }
+
+ // Returns the last subitem of the given node,
+ // creating it if necessary.
+ lastChild := func(n *TocItem) *TocItem {
+ if len(n.Items) > 0 {
+ return n.Items[len(n.Items)-1]
+ }
+ return appendChild(n)
+ }
+
+ var root TocItem
+
+ stack := []*TocItem{&root} // inv: len(stack) >= 1
+ err := ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+
+ // Skip non-heading node
+ heading, ok := n.(*ast.Heading)
+ if !ok {
+ return ast.WalkContinue, nil
+ }
+
+ if opts.MinDepth > 0 && heading.Level < opts.MinDepth {
+ return ast.WalkSkipChildren, nil
+ }
+
+ if opts.MaxDepth > 0 && heading.Level > opts.MaxDepth {
+ return ast.WalkSkipChildren, nil
+ }
+
+ // The heading is deeper than the current depth.
+ // Append empty items to match the heading's level.
+ for len(stack) < heading.Level {
+ parent := stack[len(stack)-1]
+ stack = append(stack, lastChild(parent))
+ }
+
+ // The heading is shallower than the current depth.
+ // Move back up the stack until we reach the heading's level.
+ if len(stack) > heading.Level {
+ stack = stack[:heading.Level]
+ }
+
+ parent := stack[len(stack)-1]
+ target := lastChild(parent)
+ if len(target.Title) > 0 || len(target.Items) > 0 {
+ target = appendChild(parent)
+ }
+
+ target.Title = util.UnescapePunctuations(heading.Text(src))
+ if id, ok := n.AttributeString("id"); ok {
+ target.ID, _ = id.([]byte)
+ }
+
+ return ast.WalkSkipChildren, nil
+ })
+
+ root.Items = compactItems(root.Items)
+
+ return Toc{Items: root.Items}, err
+}
+
+// compactItems removes items with no titles
+// from the given list of items.
+//
+// Children of removed items will be promoted to the parent item.
+func compactItems(items []*TocItem) []*TocItem {
+ result := make([]*TocItem, 0)
+ for _, item := range items {
+ if len(item.Title) == 0 {
+ result = append(result, compactItems(item.Items)...)
+ continue
+ }
+
+ item.Items = compactItems(item.Items)
+ result = append(result, item)
+ }
+
+ return result
+}
diff --git a/gno.land/pkg/gnoweb/public/favicon.ico b/gno.land/pkg/gnoweb/public/favicon.ico
new file mode 100644
index 00000000000..528c362c44a
Binary files /dev/null and b/gno.land/pkg/gnoweb/public/favicon.ico differ
diff --git a/gno.land/pkg/gnoweb/public/fonts/intervar/Intervar.woff2 b/gno.land/pkg/gnoweb/public/fonts/intervar/Intervar.woff2
new file mode 100644
index 00000000000..891fc5cc567
Binary files /dev/null and b/gno.land/pkg/gnoweb/public/fonts/intervar/Intervar.woff2 differ
diff --git a/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff b/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff
new file mode 100644
index 00000000000..2c58fe2d6d7
Binary files /dev/null and b/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff differ
diff --git a/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff2 b/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff2
new file mode 100644
index 00000000000..53d081f3a53
Binary files /dev/null and b/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff2 differ
diff --git a/gno.land/pkg/gnoweb/public/imgs/gnoland.svg b/gno.land/pkg/gnoweb/public/imgs/gnoland.svg
new file mode 100644
index 00000000000..30d2f3ef56a
--- /dev/null
+++ b/gno.land/pkg/gnoweb/public/imgs/gnoland.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/gno.land/pkg/gnoweb/public/js/copy.js b/gno.land/pkg/gnoweb/public/js/copy.js
new file mode 100644
index 00000000000..918a30b1ca3
--- /dev/null
+++ b/gno.land/pkg/gnoweb/public/js/copy.js
@@ -0,0 +1 @@
+var s=class o{DOM;static FEEDBACK_DELAY=750;btnClicked=null;btnClickedIcons=[];isAnimationRunning=!1;static SELECTORS={button:"[data-copy-btn]",icon:"[data-copy-icon] > use",content:t=>`[data-copy-content="${t}"]`};constructor(){this.DOM={el:document.querySelector("main")},this.DOM.el?this.init():console.warn("Copy: Main container not found.")}init(){this.bindEvents()}bindEvents(){this.DOM.el?.addEventListener("click",this.handleClick.bind(this))}handleClick(t){let e=t.target.closest(o.SELECTORS.button);if(!e)return;this.btnClicked=e,this.btnClickedIcons=Array.from(e.querySelectorAll(o.SELECTORS.icon));let i=e.getAttribute("data-copy-btn");if(!i){console.warn("Copy: No content ID found on the button.");return}let r=this.DOM.el?.querySelector(o.SELECTORS.content(i));r?this.copyToClipboard(r,this.btnClickedIcons):console.warn(`Copy: No content found for ID "${i}".`)}sanitizeContent(t){let n=t.innerHTML.replace(/]*class="chroma-ln"[^>]*>[\s\S]*?<\/span>/g,""),e=document.createElement("div");return e.innerHTML=n,e.textContent?.trim()||""}toggleIcons(t){t.forEach(n=>{n.classList.toggle("hidden")})}showFeedback(t){!this.btnClicked||this.isAnimationRunning===!0||(this.isAnimationRunning=!0,this.toggleIcons(t),window.setTimeout(()=>{this.toggleIcons(t),this.isAnimationRunning=!1},o.FEEDBACK_DELAY))}async copyToClipboard(t,n){let e=this.sanitizeContent(t);if(!navigator.clipboard){console.error("Copy: Clipboard API is not supported in this browser."),this.showFeedback(n);return}try{await navigator.clipboard.writeText(e),this.showFeedback(n)}catch(i){console.error("Copy: Error while copying text.",i),this.showFeedback(n)}}},a=()=>new s;export{a as default};
diff --git a/gno.land/pkg/gnoweb/public/js/index.js b/gno.land/pkg/gnoweb/public/js/index.js
new file mode 100644
index 00000000000..e990dd91f5f
--- /dev/null
+++ b/gno.land/pkg/gnoweb/public/js/index.js
@@ -0,0 +1 @@
+(()=>{let s={copy:{selector:"[data-copy-btn]",path:"/public/js/copy.js"},help:{selector:"#help",path:"/public/js/realmhelp.js"},searchBar:{selector:"#header-searchbar",path:"/public/js/searchbar.js"}},r=async({selector:e,path:o})=>{if(document.querySelector(e))try{(await import(o)).default()}catch(t){console.error(`Error while loading script ${o}:`,t)}else console.warn(`Module not loaded: no element matches selector "${e}"`)},l=async()=>{let e=Object.values(s).map(o=>r(o));await Promise.all(e)};document.addEventListener("DOMContentLoaded",l)})();
diff --git a/gno.land/pkg/gnoweb/public/js/realmhelp.js b/gno.land/pkg/gnoweb/public/js/realmhelp.js
new file mode 100644
index 00000000000..5d4a3feeba6
--- /dev/null
+++ b/gno.land/pkg/gnoweb/public/js/realmhelp.js
@@ -0,0 +1 @@
+function d(s,e=250){let t;return function(...a){t!==void 0&&clearTimeout(t),t=setTimeout(()=>{s.apply(this,a)},e)}}var l=class s{DOM;funcList;static SELECTORS={container:"#help",func:"[data-func]",addressInput:"[data-role='help-input-addr']",cmdModeSelect:"[data-role='help-select-mode']"};constructor(){this.DOM={el:document.querySelector(s.SELECTORS.container),funcs:[],addressInput:null,cmdModeSelect:null},this.funcList=[],this.DOM.el?this.init():console.warn("Help: Main container not found.")}init(){let{el:e}=this.DOM;e&&(this.DOM.funcs=Array.from(e.querySelectorAll(s.SELECTORS.func)),this.DOM.addressInput=e.querySelector(s.SELECTORS.addressInput),this.DOM.cmdModeSelect=e.querySelector(s.SELECTORS.cmdModeSelect),this.funcList=this.DOM.funcs.map(t=>new o(t)),this.restoreAddress(),this.bindEvents())}restoreAddress(){let{addressInput:e}=this.DOM;if(e){let t=localStorage.getItem("helpAddressInput");t&&(e.value=t,this.funcList.forEach(a=>a.updateAddr(t)))}}bindEvents(){let{addressInput:e,cmdModeSelect:t}=this.DOM,a=d(r=>{let n=r.value;localStorage.setItem("helpAddressInput",n),this.funcList.forEach(i=>i.updateAddr(n))});e?.addEventListener("input",()=>a(e)),t?.addEventListener("change",r=>{let n=r.target;this.funcList.forEach(i=>i.updateMode(n.value))})}},o=class s{DOM;funcName;static SELECTORS={address:"[data-role='help-code-address']",args:"[data-role='help-code-args']",mode:"[data-code-mode]",paramInput:"[data-role='help-param-input']"};constructor(e){this.DOM={el:e,addrs:Array.from(e.querySelectorAll(s.SELECTORS.address)),args:Array.from(e.querySelectorAll(s.SELECTORS.args)),modes:Array.from(e.querySelectorAll(s.SELECTORS.mode)),paramInputs:Array.from(e.querySelectorAll(s.SELECTORS.paramInput))},this.funcName=e.dataset.func||null,this.initializeArgs(),this.bindEvents()}static sanitizeArgsInput(e){let t=e.dataset.param||"",a=e.value.trim();return t||console.warn("sanitizeArgsInput: param is missing in arg input dataset."),{paramName:t,paramValue:a}}bindEvents(){let e=d((t,a)=>{t&&this.updateArg(t,a)});this.DOM.el.addEventListener("input",t=>{let a=t.target;if(a.dataset.role==="help-param-input"){let{paramName:r,paramValue:n}=s.sanitizeArgsInput(a);e(r,n)}})}initializeArgs(){this.DOM.paramInputs.forEach(e=>{let{paramName:t,paramValue:a}=s.sanitizeArgsInput(e);t&&this.updateArg(t,a)})}updateArg(e,t){this.DOM.args.filter(a=>a.dataset.arg===e).forEach(a=>{a.textContent=t||""})}updateAddr(e){this.DOM.addrs.forEach(t=>{t.textContent=e.trim()||"ADDRESS"})}updateMode(e){this.DOM.modes.forEach(t=>{let a=t.dataset.codeMode===e;t.classList.toggle("inline",a),t.classList.toggle("hidden",!a),t.dataset.copyContent=a?`help-cmd-${this.funcName}`:""})}},p=()=>new l;export{p as default};
diff --git a/gno.land/pkg/gnoweb/public/js/searchbar.js b/gno.land/pkg/gnoweb/public/js/searchbar.js
new file mode 100644
index 00000000000..e8012b9b6d9
--- /dev/null
+++ b/gno.land/pkg/gnoweb/public/js/searchbar.js
@@ -0,0 +1 @@
+var n=class r{DOM;baseUrl;static SELECTORS={container:"#header-searchbar",inputSearch:"[data-role='header-input-search']",breadcrumb:"[data-role='header-breadcrumb-search']"};constructor(){this.DOM={el:document.querySelector(r.SELECTORS.container),inputSearch:null,breadcrumb:null},this.baseUrl=window.location.origin,this.DOM.el?this.init():console.warn("SearchBar: Main container not found.")}init(){let{el:e}=this.DOM;this.DOM.inputSearch=e?.querySelector(r.SELECTORS.inputSearch)??null,this.DOM.breadcrumb=e?.querySelector(r.SELECTORS.breadcrumb)??null,this.DOM.inputSearch||console.warn("SearchBar: Input element for search not found."),this.bindEvents()}bindEvents(){this.DOM.el?.addEventListener("submit",e=>{e.preventDefault(),this.searchUrl()})}searchUrl(){let e=this.DOM.inputSearch?.value.trim();if(e){let t=e;/^https?:\/\//i.test(t)||(t=`${this.baseUrl}${t.startsWith("/")?"":"/"}${t}`);try{window.location.href=new URL(t).href}catch{console.error("SearchBar: Invalid URL. Please enter a valid URL starting with http:// or https://.")}}else console.error("SearchBar: Please enter a URL to search.")}},i=()=>new n;export{i as default};
diff --git a/gno.land/pkg/gnoweb/public/js/utils.js b/gno.land/pkg/gnoweb/public/js/utils.js
new file mode 100644
index 00000000000..e27fb93bc1c
--- /dev/null
+++ b/gno.land/pkg/gnoweb/public/js/utils.js
@@ -0,0 +1 @@
+function r(t,n=250){let e;return function(...i){e!==void 0&&clearTimeout(e),e=setTimeout(()=>{t.apply(this,i)},n)}}export{r as debounce};
diff --git a/gno.land/pkg/gnoweb/public/styles.css b/gno.land/pkg/gnoweb/public/styles.css
new file mode 100644
index 00000000000..8e8d7ed802d
--- /dev/null
+++ b/gno.land/pkg/gnoweb/public/styles.css
@@ -0,0 +1,3 @@
+@font-face{font-family:Roboto;font-style:normal;font-weight:900;font-display:swap;src:url(fonts/roboto/roboto-mono-normal.woff2) format("woff2"),url(fonts/roboto/roboto-mono-normal.woff) format("woff")}@font-face{font-family:Inter var;font-weight:100 900;font-display:block;font-style:oblique 0deg 10deg;src:url(fonts/intervar/Intervar.woff2) format("woff2")}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }
+
+/*! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #bdbdbd}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#7c7c7c}input::placeholder,textarea::placeholder{opacity:1;color:#7c7c7c}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));font-family:Inter var,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji,sans-serif;font-size:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;font-smoothing:antialiased;font-variant-ligatures:contextual common-ligatures;font-kerning:normal;text-rendering:optimizeLegibility}svg{max-height:100%;max-width:100%}form{margin-top:0;margin-bottom:0}.realm-content{overflow-wrap:break-word;padding-top:2.5rem;font-size:1rem}.realm-content>:first-child{margin-top:0!important}.realm-content a{font-weight:500;--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.realm-content a:hover{text-decoration-line:underline}.realm-content h1,.realm-content h2,.realm-content h3,.realm-content h4{margin-top:3rem;line-height:1.25;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content h2,.realm-content h2 *{font-weight:700}.realm-content h3,.realm-content h3 *,.realm-content h4,.realm-content h4 *{font-weight:600}.realm-content h1+h2,.realm-content h2+h3,.realm-content h3+h4{margin-top:1rem}.realm-content h1{font-size:2.375rem;font-weight:700}.realm-content h2{font-size:1.5rem}.realm-content h3{margin-top:2.5rem;font-size:1.25rem}.realm-content h3,.realm-content h4{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content h4{margin-top:1.5rem;margin-bottom:1.5rem;font-size:1.125rem;font-weight:500}.realm-content p{margin-top:1.25rem;margin-bottom:1.25rem}.realm-content strong{font-weight:700;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content strong *{font-weight:700}.realm-content em{font-style:oblique 10deg}.realm-content blockquote{margin-top:1rem;margin-bottom:1rem;border-left-width:4px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-style:oblique 10deg}.realm-content ol,.realm-content ul{margin-top:1.5rem;margin-bottom:1.5rem;padding-left:1rem}.realm-content ol li,.realm-content ul li{margin-bottom:.5rem}.realm-content img{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-content figure{margin-top:1.5rem;margin-bottom:1.5rem;text-align:center}.realm-content figcaption{font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content :not(pre)>code{border-radius:.25rem;background-color:rgb(226 226 226/var(--tw-bg-opacity));padding:.125rem .25rem;font-size:.96em}.realm-content :not(pre)>code,.realm-content pre{--tw-bg-opacity:1;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content pre{overflow-x:auto;border-radius:.375rem;background-color:rgb(240 240 240/var(--tw-bg-opacity));padding:1rem}.realm-content hr{margin-top:2.5rem;margin-bottom:2.5rem;border-top-width:1px;--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.realm-content table{margin-top:2rem;margin-bottom:2rem;display:block;width:100%;max-width:100%;border-collapse:collapse;overflow-x:auto}.realm-content td,.realm-content th{white-space:normal;overflow-wrap:break-word;border-width:1px;padding:.5rem 1rem}.realm-content th{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));font-weight:700}.realm-content caption{margin-top:.5rem;text-align:left;font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content q{margin-top:1.5rem;margin-bottom:1.5rem;border-left-width:4px;--tw-border-opacity:1;border-left-color:rgb(204 204 204/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(85 85 85/var(--tw-text-opacity));font-style:oblique 10deg;quotes:"“" "”" "‘" "’"}.realm-content q:after,.realm-content q:before{margin-right:.25rem;font-size:1.5rem;--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity));content:open-quote;vertical-align:-.4rem}.realm-content q:after{content:close-quote}.realm-content q:before{content:open-quote}.realm-content q:after{content:close-quote}.realm-content ol ol,.realm-content ol ul,.realm-content ul ol,.realm-content ul ul{margin-top:.75rem;margin-bottom:.5rem;padding-left:1rem}.realm-content ul{list-style-type:disc}.realm-content ol{list-style-type:decimal}.realm-content abbr[title]{cursor:help;border-bottom-width:1px;border-style:dotted}.realm-content details{margin-top:1.25rem;margin-bottom:1.25rem}.realm-content summary{cursor:pointer;font-weight:700}.realm-content a code{color:inherit}.realm-content video{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-content math{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content small{font-size:.875rem}.realm-content del{text-decoration-line:line-through}.realm-content sub{vertical-align:sub;font-size:.75rem}.realm-content sup{vertical-align:super;font-size:.75rem}.realm-content button,.realm-content input{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}main :is(h1,h2,h3,h4){scroll-margin-top:6rem}::-moz-selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}::selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.sidemenu .peer:checked+label>svg{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.toc-expend-btn:has(#toc-expend:checked)+nav{display:block}.toc-expend-btn:has(#toc-expend:checked) .toc-expend-btn_ico{--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.main-header:has(#sidemenu-docs:checked)+main #sidebar #sidebar-docs,.main-header:has(#sidemenu-meta:checked)+main #sidebar #sidebar-meta,.main-header:has(#sidemenu-source:checked)+main #sidebar #sidebar-source,.main-header:has(#sidemenu-summary:checked)+main #sidebar #sidebar-summary{display:block}@media (min-width:40rem){:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .main-navigation,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main .realm-content{grid-column:span 6/span 6}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .sidemenu,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar{grid-column:span 4/span 4}}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar:before{position:absolute;top:0;left:-1.75rem;z-index:-1;display:block;height:100%;width:50vw;--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));--tw-content:"";content:var(--tw-content)}main :is(.source-code)>pre{overflow:scroll;border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(255 255 255/var(--tw-bg-opacity))!important;padding:1rem .25rem;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-size:.875rem}@media (min-width:40rem){main :is(.source-code)>pre{padding:2rem .75rem;font-size:1rem}}main .realm-content>pre a:hover{text-decoration-line:none}main :is(.realm-content,.source-code)>pre .chroma-ln:target{background-color:transparent!important}main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target) .chroma-cl,main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover) .chroma-cl{border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(226 226 226/var(--tw-bg-opacity))!important}main :is(.realm-content,.source-code)>pre .chroma-ln{scroll-margin-top:6rem}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.bottom-1{bottom:.25rem}.left-0{left:0}.right-2{right:.5rem}.right-3{right:.75rem}.top-0{top:0}.top-1\/2{top:50%}.top-14{top:3.5rem}.top-2{top:.5rem}.z-1{z-index:1}.z-max{z-index:9999}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-3{grid-column:span 3/span 3}.col-span-7{grid-column:span 7/span 7}.row-span-1{grid-row:span 1/span 1}.row-start-1{grid-row-start:1}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.mr-10{margin-right:2.5rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.max-h-screen{max-height:100vh}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-full{width:100%}.min-w-2{min-width:.5rem}.min-w-48{min-width:12rem}.max-w-screen-max{max-width:98.75rem}.shrink-0{flex-shrink:0}.grow-\[2\]{flex-grow:2}.-translate-y-1\/2{--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-flow-dense{grid-auto-flow:dense}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-2{row-gap:.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.375rem}.rounded-sm{border-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-gray-100{--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(153 153 153/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.bg-light{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.font-mono{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.text-100{font-size:.875rem}.text-200{font-size:1rem}.text-50{font-size:.75rem}.text-600{font-size:1.5rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-tight{line-height:1.25}.text-gray-300{--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(124 124 124/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(19 19 19/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.text-light{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.outline-none{outline:2px solid transparent;outline-offset:2px}.text-stroke{-webkit-text-stroke:currentColor;-webkit-text-stroke-width:.6px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.\*\:pl-0>*{padding-left:0}.before\:px-\[0\.18rem\]:before{content:var(--tw-content);padding-left:.18rem;padding-right:.18rem}.before\:text-gray-300:before{content:var(--tw-content);--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.before\:content-\[\'\/\'\]:before{--tw-content:"/";content:var(--tw-content)}.before\:content-\[\'\:\'\]:before{--tw-content:":";content:var(--tw-content)}.before\:content-\[\'open\'\]:before{--tw-content:"open";content:var(--tw-content)}.after\:pointer-events-none:after{content:var(--tw-content);pointer-events:none}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:bottom-0:after{content:var(--tw-content);bottom:0}.after\:left-0:after{content:var(--tw-content);left:0}.after\:top-0:after{content:var(--tw-content);top:0}.after\:block:after{content:var(--tw-content);display:block}.after\:h-1:after{content:var(--tw-content);height:.25rem}.after\:h-full:after{content:var(--tw-content);height:100%}.after\:w-full:after{content:var(--tw-content);width:100%}.after\:rounded-t-sm:after{content:var(--tw-content);border-top-left-radius:.25rem;border-top-right-radius:.25rem}.after\:bg-gray-100:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.after\:bg-green-600:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.first\:border-t:first-child{border-top-width:1px}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.hover\:text-green-600:hover{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.hover\:text-light:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-gray-300:focus{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.focus\:border-l-gray-300:focus{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-gray-300{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-l-gray-300{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group.is-active .group-\[\.is-active\]\:text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.peer:checked~.peer-checked\:before\:content-\[\'close\'\]:before{--tw-content:"close";content:var(--tw-content)}.peer:focus-within~.peer-focus-within\:hidden{display:none}.has-\[ul\:empty\]\:hidden:has(ul:empty){display:none}.has-\[\:focus-within\]\:border-gray-300:has(:focus-within){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.has-\[\:focus\]\:border-gray-300:has(:focus){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}@media (min-width:30rem){.sm\:gap-6{gap:1.5rem}}@media (min-width:40rem){.md\:col-span-3{grid-column:span 3/span 3}.md\:mb-0{margin-bottom:0}.md\:h-4{height:1rem}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.md\:px-10{padding-left:2.5rem;padding-right:2.5rem}.md\:pb-0{padding-bottom:0}}@media (min-width:51.25rem){.lg\:order-2{order:2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:row-span-2{grid-row:span 2/span 2}.lg\:row-start-1{grid-row-start:1}.lg\:row-start-2{grid-row-start:2}.lg\:mb-4{margin-bottom:1rem}.lg\:mt-0{margin-top:0}.lg\:mt-10{margin-top:2.5rem}.lg\:block{display:block}.lg\:hidden{display:none}.lg\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:justify-start{justify-content:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.lg\:border-none{border-style:none}.lg\:bg-transparent{background-color:transparent}.lg\:p-0{padding:0}.lg\:px-0{padding-left:0;padding-right:0}.lg\:px-2{padding-left:.5rem;padding-right:.5rem}.lg\:py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.lg\:pb-28{padding-bottom:7rem}.lg\:pt-2{padding-top:.5rem}.lg\:text-200{font-size:1rem}.lg\:font-semibold{font-weight:600}.lg\:hover\:bg-transparent:hover{background-color:transparent}}@media (min-width:63.75rem){.xl\:inline{display:inline}.xl\:hidden{display:none}.xl\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:gap-20{gap:5rem}.xl\:gap-6{gap:1.5rem}.xl\:pt-0{padding-top:0}}@media (min-width:85.375rem){.xxl\:inline-block{display:inline-block}.xxl\:h-4{height:1rem}.xxl\:w-4{width:1rem}.xxl\:gap-20{gap:5rem}.xxl\:gap-x-32{-moz-column-gap:8rem;column-gap:8rem}.xxl\:pr-1{padding-right:.25rem}}
\ No newline at end of file
diff --git a/gno.land/pkg/gnoweb/static.go b/gno.land/pkg/gnoweb/static.go
new file mode 100644
index 00000000000..7900dcd7891
--- /dev/null
+++ b/gno.land/pkg/gnoweb/static.go
@@ -0,0 +1,28 @@
+package gnoweb
+
+import (
+ "embed"
+ "net/http"
+)
+
+//go:embed public/*
+var assets embed.FS
+
+func disableCache(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Cache-Control", "no-store")
+ next.ServeHTTP(w, r)
+ })
+}
+
+// AssetHandler returns the handler to serve static assets. If cache is true,
+// these will be served using the static files embedded in the binary; otherwise
+// they will served from the filesystem.
+func AssetHandler() http.Handler {
+ return http.FileServer(http.FS(assets))
+}
+
+func DevAssetHandler(path, dir string) http.Handler {
+ handler := http.StripPrefix(path, http.FileServer(http.Dir(dir)))
+ return disableCache(handler)
+}
diff --git a/gno.land/pkg/gnoweb/static/css/app.css b/gno.land/pkg/gnoweb/static/css/app.css
deleted file mode 100644
index c10fc8ec0e0..00000000000
--- a/gno.land/pkg/gnoweb/static/css/app.css
+++ /dev/null
@@ -1,862 +0,0 @@
-/**** ROBOTO ****/
-
-@font-face {
- font-family: "Roboto Mono";
- font-style: normal;
- font-weight: normal;
- font-display: swap;
- src: local("Roboto Mono Regular"), url("/static/font/roboto/RobotoMono-Regular.woff") format("woff");
- }
-
- @font-face {
- font-family: "Roboto Mono";
- font-style: italic;
- font-weight: normal;
- font-display: swap;
- src: local("Roboto Mono Italic"), url("/static/font/roboto/RobotoMono-Italic.woff") format("woff");
- }
-
- @font-face {
- font-family: "Roboto Mono Bold";
- font-style: normal;
- font-weight: 700;
- font-display: swap;
- src: local("Roboto Mono Bold"), url("/static/font/roboto/RobotoMono-Bold.woff") format("woff");
- }
-
- @font-face {
- font-family: "Roboto Mono";
- font-style: italic;
- font-weight: 700;
- font-display: swap;
- src: local("Roboto Mono Bold Italic"), url("/static/font/roboto/RobotoMono-BoldItalic.woff") format("woff");
- }
-
-
-/*** DARK/LIGHT THEME COLORS ***/
-
-html:not([data-theme="dark"]),
-html[data-theme="light"] {
- --background-color: #eee;
- --input-background-color: #eee;
- --text-color: #000;
- --link-color: #25172a;
- --muted-color: #757575;
- --border-color: #d7d9db;
- --icon-color: #000;
-
- --quote-background: #ddd;
- --quote-2-background: #aaa4;
- --code-background: #d7d9db;
- --header-background: #373737;
- --header-forground: #ffffff;
- --logo-hat: #ffffff;
- --logo-beard: #808080;
-
- --realm-help-background-color: #d7d9db9e;
- --realm-help-odd-background-color: #d7d9db45;
- --realm-help-code-color: #5d5d5d;
-
- --highlight-color: #2f3337;
- --highlight-bg: #f6f6f6;
- --highlight-color: #2f3337;
- --highlight-comment: #656e77;
- --highlight-keyword: #015692;
- --highlight-attribute: #015692;
- --highlight-symbol: #803378;
- --highlight-namespace: #b75501;
- --highlight-keyword: #015692;
- --highlight-variable: #54790d;
- --highlight-keyword: #015692;
- --highlight-literal: #b75501;
- --highlight-punctuation: #535a60;
- --highlight-variable: #54790d;
- --highlight-deletion: #c02d2e;
- --highlight-addition: #2f6f44;
-}
-
-html[data-theme="dark"] {
- --background-color: #1e1e1e;
- --input-background-color: #393939;
- --text-color: #c7c7c7;
- --link-color: #c7c7c7;
- --muted-color: #737373;
- --border-color: #606060;
- --icon-color: #dddddd;
-
- --quote-background: #404040;
- --quote-2-background: #555555;
- --code-background: #606060;
- --header-background: #373737;
- --header-forground: #ffffff;
- --logo-hat: #ffffff;
- --logo-beard: #808080;
-
- --realm-help-background-color: #45454545;
- --realm-help-odd-background-color: #4545459e;
- --realm-help-code-color: #b6b6b6;
-
- --highlight-color: #ffffff;
- --highlight-bg: #1c1b1b;
- --highlight-color: #ffffff;
- --highlight-comment: #999999;
- --highlight-keyword: #88aece;
- --highlight-attribute: #88aece;
- --highlight-symbol: #c59bc1;
- --highlight-namespace: #f08d49;
- --highlight-keyword: #88aece;
- --highlight-variable: #b5bd68;
- --highlight-keyword: #88aece;
- --highlight-literal: #f08d49;
- --highlight-punctuation: #cccccc;
- --highlight-variable: #b5bd68;
- --highlight-deletion: #de7176;
- --highlight-addition: #76c490;
-}
-
-.logo-wording path {fill: var(--header-forground, #ffffff); }
-.logo-beard { fill: var(--logo-beard, #808080); }
-.logo-hat {fill: var(--logo-hat, #ffffff); }
-
-#theme-toggle {
- cursor: pointer;
- display: inline-block;
- padding: 0;
- color: var(--header-forground, #ffffff);
-}
-
-html[data-theme="dark"] #theme-toggle-moon,
-html[data-theme="light"] #theme-toggle-sun {
- display: none;
-}
-
-/*** BASE HTML ELEMENTS ***/
-
-* {
- box-sizing: border-box;
-}
-
-html {
- font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on;
- -webkit-font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on;
- text-size-adjust: 100%;
- -moz-osx-font-smoothing: grayscale;
- font-smoothing: antialiased;
- font-variant-ligatures: contextual common-ligatures;
- font-kerning: normal;
- text-rendering: optimizeLegibility;
- -moz-text-size-adjust: none;
- -webkit-text-size-adjust: none;
- text-size-adjust: none;
-}
-
-html,
-body {
- padding: 0;
- margin: 0;
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
- "Segoe UI Symbol", "Noto Color Emoji"; background-color: var(--background-color, #eee);
- color: var(--text-color, #000);
- font-size: 15px;
- transition: 0.25s all ease;
-}
-
-h1,
-h2,
-h3,
-h4,
-nav {
-
- font-weight: 600;
- letter-spacing: 0.08rem;
-}
-
-:is(h1, h2, h3, h4) a {
- text-decoration: none;
-}
-
-h1 {
- text-align: center;
- font-size: 2rem;
- margin-block: 4.2rem 2rem;
-}
-
-h2 {
- font-size: 1.625rem;
- margin-block: 3.4rem 1.2rem;
- line-height: 1.4;
-}
-
-h3 {
- font-size: 1.467rem;
- margin-block: 2.6rem 1rem;
-}
-
-p {
- font-size: 1rem;
- margin-block: 1.2rem;
- line-height: 1.4;
-}
-
-p:last-child:has(a:only-child) {
- margin-block-start: 0.8rem;
-}
-.stack > p:last-child:has(a:only-child) {
- margin-block-start: 0;
-}
-
-hr {
- border: none;
- height: 1px;
- background: var(--border-color, #d7d9db);
- width: 100%;
- margin-block: 1.5rem 2rem;
-}
-
-nav {
- font-weight: 400;
-}
-
-button {
- color: var(--text-color, #000);
-}
-
-body {
- height: 100%;
- width: 100%;
-}
-
-input {
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: none;
-}
-
-a {
- color: var(--link-color, #25172a);
-}
-
-a[href="#"] {
- color: var(--muted-color, #757575);
-}
-
-.gno-tmpl-section ul {
- padding: 0;
-}
-
-.gno-tmpl-section li ,
-#header li ,
-.footer li {
- list-style: none;
-}
-
-.gno-tmpl-section blockquote {
- margin-inline: 0;
-}
-
-li {
- margin-bottom: 0.4rem;
-}
-
-li > * {
- vertical-align: middle;
-}
-
-input {
- background-color: var(--input-background-color, #eee);
- border: 1px solid var(--border-color);
- color: var(--text-color, #000);
- width: 25em;
- padding: 0.4rem 0.5rem;
- max-width: 100%;x
-}
-
-blockquote {
- background-color: var(--quote-background, #ddd);
-}
-
-blockquote blockquote {
- margin: 0;
- background-color: var(--quote-2-background, #aaa4);
-}
-
-pre, code {
- font-family: "Roboto Mono", "Courier New", "sans-serif";
-}
-pre {
- background-color: var(--code-background, #d7d9db);
- margin: 0;
- padding: 0.5rem;
-}
-
-label {
- margin-block-end: 0.8rem;
- display: block;
-}
-
-label > img {
- margin-inline-end: 0.8rem;
-}
-
-code {
- white-space: pre-wrap;
- overflow-wrap: anywhere;
-}
-/*** COMPOSITION ***/
-.container {
- width: 100%;
- max-width: 63.75rem;
- margin: auto;
- padding: 1.25rem;
-}
-
-.container p > img:only-child {
- max-width: 100%;
-}
-.gno-tmpl-page p img:only-child {
- margin-inline: auto;
- display: block;
- max-width: 100%;
-}
-
-.inline-list {
- padding: 1rem;
- display: flex;
- justify-content: space-between;
-}
-
-
-
-.stack,
-.stack > p {
- display: flex;
- flex-direction: column;
-}
-
-.stack > p {
- margin: 0;
-}
-
-.stack > a,
-.stack > p > a{
- margin-block-end: 0.4rem;
-}
-
-.column > h1,
-.column > h2,
-.column > h3,
-.column > h4,
-.column > h5,
-.column > h6 {
- margin-block-start: 0;
-}
-
-.columns-2,
-.columns-3 {
- display: grid;
- grid-template-columns: repeat(1, 1fr);
- grid-gap: 3.75rem;
- margin: 3.75rem auto;
-}
-
-.footer {
- text-align: center;
- margin-block-start: 2rem;
- background-color: var(--header-background, #d7d9db);
- border-top: 1px solid var(--border-color);
-}
-
-.footer > .logo {
- display: inline-block;
- margin: 1rem;
- height: 1.2rem;
-}
-
-/** 51.2rem **/
-@media screen and (min-width: 68.75rem) {
- .stack,
- .stack > p {
- flex-direction: row;
- }
- .stack *:not(:first-child) {
- margin-left: 3.75rem;
- }
- .stack > a,
- .stack > p > a{
- margin-block-end: 0;
- }
- .columns-2 {
- grid-template-columns: repeat(2, 1fr);
- }
- .columns-3 {
- grid-template-columns: repeat(3, 1fr);
- }
-}
-
-/*** UTILITIES ***/
-
-.is-hidden {
- display: none;
-}
-
-.is-muted {
- color: var(--muted-color, #757575);
-}
-
-.is-finished {
- text-decoration: line-through;
-}
-
-.is-underline {
- text-decoration: underline;
-}
-
-/*** BLOCKS ***/
-.tabs button {
- border: none;
- cursor: pointer;
- text-decoration: underline;
- padding: 0;
- background: none;
- color: var(--text-color, #000);
-}
-
-.tabs button[aria-selected="true"] {
- font-weight: 700;
-}
-
-.tabs + .jumbotron {
- margin-top: 2.5rem;
-}
-.tabs > .columns-2,
-.tabs > .columns-3 {
- margin-bottom: 2.5rem;
-}
-
-.accordion-trigger {
- display: block;
- border: none;
- cursor: pointer;
- padding: 0.4rem 0;
- font-size: 1.125rem;
- font-weight: 700;
- text-align: left;
- background: none;
-}
-
-.accordion-trigger ~ div {
- padding: 0.875rem 0 2.2rem;
-}
-
-.accordion > p {
- margin-block: 0;
-}
-/** 51.2rem **/
-@media screen and (min-width: 68.75rem) {
- .accordion .accordion-trigger ~ div {
- padding: 0.875rem 0 2.2rem 2rem;
- }
-}
-
-.gor-accordion button::first-letter {
- font-size: 1.5em;
- color: var(--text-color, #000);
-}
-
-.jumbotron {
- border: 1px solid var(--border-color, #d7d9db);
- padding: 1.4rem;
- margin: 3.75rem auto;
-}
-
-.jumbotron h1 {
- text-align: left;
-}
-
-.jumbotron > *:first-child,
-.jumbotron > * > *:first-child {
- margin-block-start: 0;
-}
-
-.jumbotron > *:last-child,
-.jumbotron > * > *:last-child {
- margin-block-end: 0;
-}
-
-/** 68.75rem**/
-@media screen and (min-width: 68.75rem) {
- .jumbotron {
- margin: 3.75rem -3.5rem;
- padding: 3.5rem;
- }
-}
-
-#root {
- display: flex;
- flex-direction: column;
- border: 1px solid var(--header-background, #d7d9db);
- margin: 20px;
- overflow: hidden;
- /* height: calc(100vh - 40px); */
-}
-
-#header {
- position: relative;
- background-color: var(--header-background, #d7d9db);
- padding: 1.333rem;
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-#header > nav {
- flex-grow: 2;
-}
-
-#header .logo {
- display: flex;
- align-items: center;
- color: var(--link-color, #25172a);
- position: absolute;
- height: 2.4rem;
- z-index: 2;
-}
-
-.logo > svg {
- height: 100%;
-}
-
-#logo_path a {
- text-decoration: none;
-}
-
-#logo_path {
- padding-right: 0.8rem;
-}
-
-#logo_path a:hover {
- text-decoration: underline;
-}
-
-#realm_links a {
- font-size: 0.8rem;
-}
-
-#header_buttons {
- position: relative;
- width: 100%;
- height: 3rem;
-}
-
-#header_buttons nav {
- height: 100%;
- display: flex;
- justify-content: flex-end;
- align-items: center;
-}
-
-/* enabled conditionally with */
-#source {
- opacity: 0;
-}
-
-/*** REALM HELP ***/
-#realm_render,
-#realm_help {
- padding: 0 1.467rem;
-}
-
-#realm_help .func_specs {
- box-sizing: border-box;
- width: calc(100% + 1.467rem);
- margin-left: -1.467rem;
-}
-
-#realm_help .func_spec {
- padding: 1.467rem;
- background: var(--realm-help-background-color, #d7d9db9e);
- margin-top: 1.467rem;
-}
-#realm_help .func_spec:nth-child(odd) {
- background: var(--realm-help-odd-background-color, #d7d9db45);
-}
-
-#realm_help .func_spec > table > tbody > tr > th {
- width: 3.333rem;
- vertical-align: top;
- text-align: right;
- color: var(--text-color, #000);
-}
-
-#realm_help .func_spec > table th,
-#realm_help .func_spec > table td {
- padding-bottom: 16px;
-}
-
-#realm_help .func_spec > table th + td {
- padding-left: 1rem;
-}
-
-#realm_help .func_spec > table th + td table td {
- padding-left: 0.8rem;
-}
-
-#realm_help .func_spec .shell_command {
- color: var(--realm-help-code-color, #5d5d5d);
-}
-
-#realm_help .func_name td {
- font-weight: bold;
-}
-
-/** menu **/
-#menu-toggle {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- position: relative;
- z-index: 1;
- -webkit-user-select: none;
- user-select: none;
- height: 100%;
-}
-
-#menu-toggle input {
- display: block;
- width: 2.667rem;
- height: 2.133rem;
- position: absolute;
- cursor: pointer;
- opacity: 0;
- z-index: 2;
- -webkit-touch-callout: none;
-}
-
-#menu-toggle span {
- display: block;
- width: 2.2rem;
- height: 0.133rem;
- margin-bottom: 0.333rem;
- position: relative;
- background: var(--header-forground, #ffffff);
- z-index: 1;
- transform-origin: 50% 50%;
- /* transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease; */
-}
-
-#menu-toggle input:checked ~ span {
- opacity: 1;
- transform: rotate(45deg) translate(0.467rem, 0.467rem);
-}
-
-#menu-toggle input:checked ~ span:nth-last-child(3) {
- opacity: 0;
- transform: rotate(0deg) scale(0.2, 0.2);
-}
-
-#menu-toggle input:checked ~ span:nth-last-child(2) {
- transform: rotate(-45deg) translate(0.2rem, -0.267rem);
-}
-
-#menu-toggle > .navigation {
- position: absolute;
- width: calc(100vw - 2.6rem);
- right: -1.467rem;
- top: -1.467rem;
- padding: 7.333rem 1.467rem 4.333rem;
- background: var(--header-background, #d7d9db);
- list-style-type: none;
- -webkit-font-smoothing: antialiased;
- transform-origin: 0% 0%;
- transform: translate(0, -100%);
- /* transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1); */
-}
-
-#menu-toggle .buttons {
- position: absolute;
- left: 1.467rem;
- display: flex;
- align-items: center;
- margin-block-start: .7em;
- color: var(--icon-color, #000);
-}
-
-#menu-toggle .buttons > *:not(:last-child) {
- margin-right: 0.6rem;
-}
-
-#menu-toggle .buttons button {
- background-color: transparent;
- border: none;
-}
-
-#menu-toggle .buttons a {
- text-decoration: none;
-}
-
-#menu-toggle > .navigation > ul {
- display: flex;
- flex-direction: column;
- margin-bottom: 2rem;
- margin-top: .8rem;
-}
-
-#menu-toggle > .navigation a {
- color: var(--header-forground, #ffffff);
-}
-
-#menu-toggle input:checked ~ .navigation {
- transform: none;
-}
-
-/** 51.2rem **/
-@media screen and (min-width: 51.2rem) {
- #menu-toggle {
- display: block;
- width: 50%;
- float: right;
- }
- #menu-toggle span,
- #menu-toggle input {
- display: none;
- }
- #menu-toggle > .navigation {
- position: relative;
- display: flex;
- align-items: center;
- justify-content: space-between;
- width: auto;
- padding: 0;
- right: initial;
- top: initial;
- background: transparent;
- transform: translate(0, 0);
- }
- #menu-toggle .buttons {
- right: 0;
- left: initial;
- }
- #menu-toggle > .navigation > ul {
- transform: translateX(-50%);
- flex-direction: row;
- margin-bottom: 0;
- }
- #menu-toggle > .navigation li:not(:first-child) {
- margin-left: min(1vw, 1.5rem);
- }
-}
-
-/*** EXCEPTIONS ***/
-.stack--center {
- justify-content: center;
-}
-
-.stack--thin *:not(:first-child) {
- margin-left: 1.5rem;
-}
-
-/*** HLJS ***/
-
-/* Copyright (c) 2006, Ivan Sagalaev.
- * https://github.com/highlightjs/highlight.js/blob/86dcb210227ef130a00b5ece50605ea1ec887be8/src/styles/default.css */
-pre code.hljs {
- display: block;
- overflow-x: auto;
- padding: 1em;
-}
-
-code.hljs {
- padding: 3px 5px;
-}
-
-/* Copyright 2017-2020 Stack Exchange Inc.
- * https://github.com/highlightjs/highlight.js/blob/86dcb210227ef130a00b5ece50605ea1ec887be8/src/styles/stackoverflow-light.css
- * https://github.com/highlightjs/highlight.js/blob/86dcb210227ef130a00b5ece50605ea1ec887be8/src/styles/stackoverflow-dark.css */
-.hljs {
- color: var(--highlight-color, #2f3337);
- background: var(--highlight-bg, #f6f6f6);
-}
-
-.hljs-subst {
- color: var(--highlight-color, #2f3337);
-}
-
-.hljs-comment {
- color: var(--highlight-comment, #656e77);
-}
-
-.hljs-keyword,
-.hljs-selector-tag,
-.hljs-meta .hljs-keyword,
-.hljs-doctag,
-.hljs-section {
- color: var(--highlight-keyword, #015692);
-}
-
-.hljs-attr {
- color: var(--highlight-attribute, #015692);
-}
-
-.hljs-attribute {
- color: var(--highlight-symbol, #803378);
-}
-
-.hljs-name,
-.hljs-type,
-.hljs-number,
-.hljs-selector-id,
-.hljs-quote,
-.hljs-template-tag {
- color: var(--highlight-namespace, #b75501);
-}
-
-.hljs-selector-class {
- color: var(--highlight-keyword, #015692);
-}
-
-.hljs-string,
-.hljs-regexp,
-.hljs-symbol,
-.hljs-variable,
-.hljs-template-variable,
-.hljs-link,
-.hljs-selector-attr {
- color: var(--highlight-variable, #54790d);
-}
-
-.hljs-meta,
-.hljs-selector-pseudo {
- color: var(--highlight-keyword, #015692);
-}
-
-.hljs-built_in,
-.hljs-title,
-.hljs-literal {
- color: var(--highlight-literal, #b75501);
-}
-
-.hljs-bullet,
-.hljs-code {
- color: var(--highlight-punctuation, #535a60);
-}
-
-.hljs-meta .hljs-string {
- color: var(--highlight-variable, #54790d);
-}
-
-.hljs-deletion {
- color: var(--highlight-deletion, #c02d2e);
-}
-
-.hljs-addition {
- color: var(--highlight-addition, #2f6f44);
-}
-
-.hljs-emphasis {
- font-style: italic;
-}
-
-.hljs-strong {
- font-weight: bold;
-}
diff --git a/gno.land/pkg/gnoweb/static/css/normalize.css b/gno.land/pkg/gnoweb/static/css/normalize.css
deleted file mode 100644
index 195264e4366..00000000000
--- a/gno.land/pkg/gnoweb/static/css/normalize.css
+++ /dev/null
@@ -1,379 +0,0 @@
-/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
-
-/* Document
- ========================================================================== */
-
-/**
- * 1. Correct the line height in all browsers.
- * 2. Prevent adjustments of font size after orientation changes in iOS.
- */
-
-html {
- line-height: 1.15;
- /* 1 */
- -webkit-text-size-adjust: 100%;
- /* 2 */
-}
-
-/* Sections
- ========================================================================== */
-
-/**
- * Remove the margin in all browsers.
- */
-
-body {
- margin: 0;
-}
-
-/**
- * Render the `main` element consistently in IE.
- */
-
-main {
- display: block;
-}
-
-/**
- * Correct the font size and margin on `h1` elements within `section` and
- * `article` contexts in Chrome, Firefox, and Safari.
- */
-
-h1 {
- font-size: 2em;
- margin: 0.67em 0;
-}
-
-/* Grouping content
- ========================================================================== */
-
-/**
- * 1. Add the correct box sizing in Firefox.
- * 2. Show the overflow in Edge and IE.
- */
-
-hr {
- box-sizing: content-box;
- /* 1 */
- height: 0;
- /* 1 */
- overflow: visible;
- /* 2 */
-}
-
-/**
- * 1. Correct the inheritance and scaling of font size in all browsers.
- * 2. Correct the odd `em` font sizing in all browsers.
- */
-
-pre {
- font-family: monospace, monospace;
- /* 1 */
- font-size: 1em;
- /* 2 */
-}
-
-/* Text-level semantics
- ========================================================================== */
-
-/**
- * Remove the gray background on active links in IE 10.
- */
-
-a {
- background-color: transparent;
-}
-
-/**
- * 1. Remove the bottom border in Chrome 57-
- * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
- */
-
-abbr[title] {
- border-bottom: none;
- /* 1 */
- text-decoration: underline;
- /* 2 */
- text-decoration: underline dotted;
- /* 2 */
-}
-
-/**
- * Add the correct font weight in Chrome, Edge, and Safari.
- */
-
-b,
-strong {
- font-weight: bolder;
-}
-
-/**
- * 1. Correct the inheritance and scaling of font size in all browsers.
- * 2. Correct the odd `em` font sizing in all browsers.
- */
-
-code,
-kbd,
-samp {
- font-family: monospace, monospace;
- /* 1 */
- font-size: 1em;
- /* 2 */
-}
-
-/**
- * Add the correct font size in all browsers.
- */
-
-small {
- font-size: 80%;
-}
-
-/**
- * Prevent `sub` and `sup` elements from affecting the line height in
- * all browsers.
- */
-
-sub,
-sup {
- font-size: 75%;
- line-height: 0;
- position: relative;
- vertical-align: baseline;
-}
-
-sub {
- bottom: -0.25em;
-}
-
-sup {
- top: -0.5em;
-}
-
-/* Embedded content
- ========================================================================== */
-
-/**
- * Remove the border on images inside links in IE 10.
- */
-
-img {
- border-style: none;
-}
-
-/* Forms
- ========================================================================== */
-
-/**
- * 1. Change the font styles in all browsers.
- * 2. Remove the margin in Firefox and Safari.
- */
-
-button,
-input,
-optgroup,
-select,
-textarea {
- font-family: inherit;
- /* 1 */
- font-size: 100%;
- /* 1 */
- line-height: 1.15;
- /* 1 */
- margin: 0;
- /* 2 */
-}
-
-/**
- * Show the overflow in IE.
- * 1. Show the overflow in Edge.
- */
-
-button,
-input {
- /* 1 */
- overflow: visible;
-}
-
-/**
- * Remove the inheritance of text transform in Edge, Firefox, and IE.
- * 1. Remove the inheritance of text transform in Firefox.
- */
-
-button,
-select {
- /* 1 */
- text-transform: none;
-}
-
-/**
- * Correct the inability to style clickable types in iOS and Safari.
- */
-
-button,
-[type="button"],
-[type="reset"],
-[type="submit"] {
- -webkit-appearance: button;
-}
-
-/**
- * Remove the inner border and padding in Firefox.
- */
-
-button::-moz-focus-inner,
-[type="button"]::-moz-focus-inner,
-[type="reset"]::-moz-focus-inner,
-[type="submit"]::-moz-focus-inner {
- border-style: none;
- padding: 0;
-}
-
-/**
- * Restore the focus styles unset by the previous rule.
- */
-
-button:-moz-focusring,
-[type="button"]:-moz-focusring,
-[type="reset"]:-moz-focusring,
-[type="submit"]:-moz-focusring {
- outline: 1px dotted ButtonText;
-}
-
-/**
- * Correct the padding in Firefox.
- */
-
-fieldset {
- padding: 0.35em 0.75em 0.625em;
-}
-
-/**
- * 1. Correct the text wrapping in Edge and IE.
- * 2. Correct the color inheritance from `fieldset` elements in IE.
- * 3. Remove the padding so developers are not caught out when they zero out
- * `fieldset` elements in all browsers.
- */
-
-legend {
- box-sizing: border-box;
- /* 1 */
- color: inherit;
- /* 2 */
- display: table;
- /* 1 */
- max-width: 100%;
- /* 1 */
- padding: 0;
- /* 3 */
- white-space: normal;
- /* 1 */
-}
-
-/**
- * Add the correct vertical alignment in Chrome, Firefox, and Opera.
- */
-
-progress {
- vertical-align: baseline;
-}
-
-/**
- * Remove the default vertical scrollbar in IE 10+.
- */
-
-textarea {
- overflow: auto;
-}
-
-/**
- * 1. Add the correct box sizing in IE 10.
- * 2. Remove the padding in IE 10.
- */
-
-[type="checkbox"],
-[type="radio"] {
- box-sizing: border-box;
- /* 1 */
- padding: 0;
- /* 2 */
-}
-
-/**
- * Correct the cursor style of increment and decrement buttons in Chrome.
- */
-
-[type="number"]::-webkit-inner-spin-button,
-[type="number"]::-webkit-outer-spin-button {
- height: auto;
-}
-
-/**
- * 1. Correct the odd appearance in Chrome and Safari.
- * 2. Correct the outline style in Safari.
- */
-
-[type="search"] {
- -webkit-appearance: textfield;
- /* 1 */
- outline-offset: -2px;
- /* 2 */
-}
-
-/**
- * Remove the inner padding in Chrome and Safari on macOS.
- */
-
-[type="search"]::-webkit-search-decoration {
- -webkit-appearance: none;
-}
-
-/**
- * 1. Correct the inability to style clickable types in iOS and Safari.
- * 2. Change font properties to `inherit` in Safari.
- */
-
-::-webkit-file-upload-button {
- -webkit-appearance: button;
- /* 1 */
- font: inherit;
- /* 2 */
-}
-
-/* Interactive
- ========================================================================== */
-
-/*
- * Add the correct display in Edge, IE 10+, and Firefox.
- */
-
-details {
- display: block;
-}
-
-/*
- * Add the correct display in all browsers.
- */
-
-summary {
- display: list-item;
-}
-
-/* Misc
- ========================================================================== */
-
-/**
- * Add the correct display in IE 10+.
- */
-
-template {
- display: none;
-}
-
-/**
- * Add the correct display in IE 10.
- */
-
-[hidden] {
- display: none;
-}
diff --git a/gno.land/pkg/gnoweb/static/font/README.md b/gno.land/pkg/gnoweb/static/font/README.md
deleted file mode 100644
index a077b97d304..00000000000
--- a/gno.land/pkg/gnoweb/static/font/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# FONTS
-
-This is the source repository for gno.land fonts: Roboto. Both are Open source and available from Google Fonts.
-
-- RobotoMono: Apache 2.0 — [https://fonts.google.com/specimen/Roboto+Mono/about](https://fonts.google.com/specimen/Roboto+Mono/about)
diff --git a/gno.land/pkg/gnoweb/static/font/roboto/LICENSE.txt b/gno.land/pkg/gnoweb/static/font/roboto/LICENSE.txt
deleted file mode 100644
index c61b66391a3..00000000000
--- a/gno.land/pkg/gnoweb/static/font/roboto/LICENSE.txt
+++ /dev/null
@@ -1,201 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
-TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
-2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
-3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
-4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
-5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
-6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
-7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
-8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
-9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
-END OF TERMS AND CONDITIONS
-
-APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
-Copyright [yyyy] [name of copyright owner]
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Bold.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Bold.woff
deleted file mode 100644
index c500c3857d6..00000000000
Binary files a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Bold.woff and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-BoldItalic.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-BoldItalic.woff
deleted file mode 100644
index 9ed406ab932..00000000000
Binary files a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-BoldItalic.woff and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Italic.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Italic.woff
deleted file mode 100644
index 5beafc7c57e..00000000000
Binary files a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Italic.woff and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Light.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Light.woff
deleted file mode 100644
index 68e61b1298d..00000000000
Binary files a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Light.woff and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-LightItalic.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-LightItalic.woff
deleted file mode 100644
index 2a50c6d2fd4..00000000000
Binary files a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-LightItalic.woff and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Medium.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Medium.woff
deleted file mode 100644
index e65d5310e45..00000000000
Binary files a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Medium.woff and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-MediumItalic.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-MediumItalic.woff
deleted file mode 100644
index a556d2fb729..00000000000
Binary files a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-MediumItalic.woff and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Regular.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Regular.woff
deleted file mode 100644
index 3de661ee0d9..00000000000
Binary files a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Regular.woff and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Thin.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Thin.woff
deleted file mode 100644
index 6ca15f36f5c..00000000000
Binary files a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Thin.woff and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-ThinItalic.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-ThinItalic.woff
deleted file mode 100644
index e8653ff4791..00000000000
Binary files a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-ThinItalic.woff and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/img/apple-touch-icon.png b/gno.land/pkg/gnoweb/static/img/apple-touch-icon.png
deleted file mode 100644
index dcc70338eaa..00000000000
Binary files a/gno.land/pkg/gnoweb/static/img/apple-touch-icon.png and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/img/favicon-16x16.png b/gno.land/pkg/gnoweb/static/img/favicon-16x16.png
deleted file mode 100644
index ee407d9a7ea..00000000000
Binary files a/gno.land/pkg/gnoweb/static/img/favicon-16x16.png and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/img/favicon-32x32.png b/gno.land/pkg/gnoweb/static/img/favicon-32x32.png
deleted file mode 100644
index 86dcfed925c..00000000000
Binary files a/gno.land/pkg/gnoweb/static/img/favicon-32x32.png and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/img/github-mark-32px.png b/gno.land/pkg/gnoweb/static/img/github-mark-32px.png
deleted file mode 100644
index 8b25551a979..00000000000
Binary files a/gno.land/pkg/gnoweb/static/img/github-mark-32px.png and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/img/github-mark-64px.png b/gno.land/pkg/gnoweb/static/img/github-mark-64px.png
deleted file mode 100644
index 182a1a3f734..00000000000
Binary files a/gno.land/pkg/gnoweb/static/img/github-mark-64px.png and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/img/ico-discord.svg b/gno.land/pkg/gnoweb/static/img/ico-discord.svg
deleted file mode 100644
index 2f73278fbe9..00000000000
--- a/gno.land/pkg/gnoweb/static/img/ico-discord.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/gno.land/pkg/gnoweb/static/img/ico-email.svg b/gno.land/pkg/gnoweb/static/img/ico-email.svg
deleted file mode 100644
index ff397fe664d..00000000000
--- a/gno.land/pkg/gnoweb/static/img/ico-email.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/gno.land/pkg/gnoweb/static/img/ico-telegram.svg b/gno.land/pkg/gnoweb/static/img/ico-telegram.svg
deleted file mode 100644
index 32932830dc3..00000000000
--- a/gno.land/pkg/gnoweb/static/img/ico-telegram.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/gno.land/pkg/gnoweb/static/img/ico-twitter.svg b/gno.land/pkg/gnoweb/static/img/ico-twitter.svg
deleted file mode 100644
index cf666e3842d..00000000000
--- a/gno.land/pkg/gnoweb/static/img/ico-twitter.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/gno.land/pkg/gnoweb/static/img/ico-youtube.svg b/gno.land/pkg/gnoweb/static/img/ico-youtube.svg
deleted file mode 100644
index 36efdd185f0..00000000000
--- a/gno.land/pkg/gnoweb/static/img/ico-youtube.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/gno.land/pkg/gnoweb/static/img/list-alt.png b/gno.land/pkg/gnoweb/static/img/list-alt.png
deleted file mode 100644
index 14296a4d28f..00000000000
Binary files a/gno.land/pkg/gnoweb/static/img/list-alt.png and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/img/list.png b/gno.land/pkg/gnoweb/static/img/list.png
deleted file mode 100644
index efe3934567f..00000000000
Binary files a/gno.land/pkg/gnoweb/static/img/list.png and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/img/logo-square.png b/gno.land/pkg/gnoweb/static/img/logo-square.png
deleted file mode 100644
index ce8c93d0c8a..00000000000
Binary files a/gno.land/pkg/gnoweb/static/img/logo-square.png and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/img/logo-square.svg b/gno.land/pkg/gnoweb/static/img/logo-square.svg
deleted file mode 100644
index e6ab42f5ad4..00000000000
--- a/gno.land/pkg/gnoweb/static/img/logo-square.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/gno.land/pkg/gnoweb/static/img/logo-v1.png b/gno.land/pkg/gnoweb/static/img/logo-v1.png
deleted file mode 100644
index 702fce47a52..00000000000
Binary files a/gno.land/pkg/gnoweb/static/img/logo-v1.png and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/img/og-gnoland.png b/gno.land/pkg/gnoweb/static/img/og-gnoland.png
deleted file mode 100644
index 9872fea17da..00000000000
Binary files a/gno.land/pkg/gnoweb/static/img/og-gnoland.png and /dev/null differ
diff --git a/gno.land/pkg/gnoweb/static/img/safari-pinned-tab.svg b/gno.land/pkg/gnoweb/static/img/safari-pinned-tab.svg
deleted file mode 100644
index 0005420c58d..00000000000
--- a/gno.land/pkg/gnoweb/static/img/safari-pinned-tab.svg
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-Created by potrace 1.14, written by Peter Selinger 2001-2017
-
-
-
-
-
diff --git a/gno.land/pkg/gnoweb/static/invites.txt b/gno.land/pkg/gnoweb/static/invites.txt
deleted file mode 100644
index 7bef15f954f..00000000000
--- a/gno.land/pkg/gnoweb/static/invites.txt
+++ /dev/null
@@ -1,48 +0,0 @@
-g1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s:1
-g13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8:1
-g1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q:1
-g1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj:1
-g18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0:1
-g19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz:1
-g187982000zsc493znqt828s90cmp6hcp2erhu6m:1
-g1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl:1
-g16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037:1
-g1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5:1
-g1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr:1
-g1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz:1
-g19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w:1
-g1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz:1
-g14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3:1
-g1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0:1
-g15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n:1
-g1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac:1
-g1z629z04f85k4t5gnkk5egpxw9tqxeec435esap:1
-g1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv:1
-g152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv:1
-g1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq:1
-g1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6:1
-g1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q:1
-g1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7:1
-g1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k:1
-g13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll:1
-g19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd:1
-g1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64:1
-g1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw:1
-g19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a:1
-g1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc:1
-g13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6:1
-g1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6:1
-g1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9:1
-g1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea:1
-g1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3:1
-g1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp:1
-g14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5:1
-g19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf:1
-g1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g:1
-g1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r:1
-g1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su:1
-g1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69:1
-g1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6:1
-g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq:10
-g14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa:10
-g14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t:5
diff --git a/gno.land/pkg/gnoweb/static/js/highlight.min.js b/gno.land/pkg/gnoweb/static/js/highlight.min.js
deleted file mode 100644
index 5135b77ab5b..00000000000
--- a/gno.land/pkg/gnoweb/static/js/highlight.min.js
+++ /dev/null
@@ -1,331 +0,0 @@
-/*!
- Highlight.js v11.9.0 (git: b7ec4bfafc)
- (c) 2006-2024 undefined and other contributors
- License: BSD-3-Clause
- */
- var hljs=function(){"use strict";function e(t){
- return t instanceof Map?t.clear=t.delete=t.set=()=>{
- throw Error("map is read-only")}:t instanceof Set&&(t.add=t.clear=t.delete=()=>{
- throw Error("set is read-only")
- }),Object.freeze(t),Object.getOwnPropertyNames(t).forEach((n=>{
- const i=t[n],s=typeof i;"object"!==s&&"function"!==s||Object.isFrozen(i)||e(i)
- })),t}class t{constructor(e){
- void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1}
- ignoreMatch(){this.isMatchIgnored=!0}}function n(e){
- return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")
- }function i(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t]
- ;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const s=e=>!!e.scope
- ;class o{constructor(e,t){
- this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){
- this.buffer+=n(e)}openNode(e){if(!s(e))return;const t=((e,{prefix:t})=>{
- if(e.startsWith("language:"))return e.replace("language:","language-")
- ;if(e.includes(".")){const n=e.split(".")
- ;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ")
- }return`${t}${e}`})(e.scope,{prefix:this.classPrefix});this.span(t)}
- closeNode(e){s(e)&&(this.buffer+=" ")}value(){return this.buffer}span(e){
- this.buffer+=``}}const r=(e={})=>{const t={children:[]}
- ;return Object.assign(t,e),t};class a{constructor(){
- this.rootNode=r(),this.stack=[this.rootNode]}get top(){
- return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){
- this.top.children.push(e)}openNode(e){const t=r({scope:e})
- ;this.add(t),this.stack.push(t)}closeNode(){
- if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){
- for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}
- walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){
- return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t),
- t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){
- "string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{
- a._collapse(e)})))}}class c extends a{constructor(e){super(),this.options=e}
- addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){
- this.closeNode()}__addSublanguage(e,t){const n=e.root
- ;t&&(n.scope="language:"+t),this.add(n)}toHTML(){
- return new o(this,this.options).value()}finalize(){
- return this.closeAllNodes(),!0}}function l(e){
- return e?"string"==typeof e?e:e.source:null}function g(e){return h("(?=",e,")")}
- function u(e){return h("(?:",e,")*")}function d(e){return h("(?:",e,")?")}
- function h(...e){return e.map((e=>l(e))).join("")}function f(...e){const t=(e=>{
- const t=e[e.length-1]
- ;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{}
- })(e);return"("+(t.capture?"":"?:")+e.map((e=>l(e))).join("|")+")"}
- function p(e){return RegExp(e.toString()+"|").exec("").length-1}
- const b=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./
- ;function m(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n
- ;let i=l(e),s="";for(;i.length>0;){const e=b.exec(i);if(!e){s+=i;break}
- s+=i.substring(0,e.index),
- i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?s+="\\"+(Number(e[1])+t):(s+=e[0],
- "("===e[0]&&n++)}return s})).map((e=>`(${e})`)).join(t)}
- const E="[a-zA-Z]\\w*",x="[a-zA-Z_]\\w*",w="\\b\\d+(\\.\\d+)?",y="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",_="\\b(0b[01]+)",O={
- begin:"\\\\[\\s\\S]",relevance:0},v={scope:"string",begin:"'",end:"'",
- illegal:"\\n",contains:[O]},k={scope:"string",begin:'"',end:'"',illegal:"\\n",
- contains:[O]},N=(e,t,n={})=>{const s=i({scope:"comment",begin:e,end:t,
- contains:[]},n);s.contains.push({scope:"doctag",
- begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",
- end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0})
- ;const o=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/)
- ;return s.contains.push({begin:h(/[ ]+/,"(",o,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),s
- },S=N("//","$"),M=N("/\\*","\\*/"),R=N("#","$");var j=Object.freeze({
- __proto__:null,APOS_STRING_MODE:v,BACKSLASH_ESCAPE:O,BINARY_NUMBER_MODE:{
- scope:"number",begin:_,relevance:0},BINARY_NUMBER_RE:_,COMMENT:N,
- C_BLOCK_COMMENT_MODE:M,C_LINE_COMMENT_MODE:S,C_NUMBER_MODE:{scope:"number",
- begin:y,relevance:0},C_NUMBER_RE:y,END_SAME_AS_BEGIN:e=>Object.assign(e,{
- "on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{
- t.data._beginMatch!==e[1]&&t.ignoreMatch()}}),HASH_COMMENT_MODE:R,IDENT_RE:E,
- MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+x,relevance:0},
- NUMBER_MODE:{scope:"number",begin:w,relevance:0},NUMBER_RE:w,
- PHRASAL_WORDS_MODE:{
- begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
- },QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/,
- end:/\/[gimuy]*/,contains:[O,{begin:/\[/,end:/\]/,relevance:0,contains:[O]}]},
- RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",
- SHEBANG:(e={})=>{const t=/^#![ ]*\//
- ;return e.binary&&(e.begin=h(t,/.*\b/,e.binary,/\b.*/)),i({scope:"meta",begin:t,
- end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)},
- TITLE_MODE:{scope:"title",begin:E,relevance:0},UNDERSCORE_IDENT_RE:x,
- UNDERSCORE_TITLE_MODE:{scope:"title",begin:x,relevance:0}});function A(e,t){
- "."===e.input[e.index-1]&&t.ignoreMatch()}function I(e,t){
- void 0!==e.className&&(e.scope=e.className,delete e.className)}function T(e,t){
- t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",
- e.__beforeBegin=A,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,
- void 0===e.relevance&&(e.relevance=0))}function L(e,t){
- Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function B(e,t){
- if(e.match){
- if(e.begin||e.end)throw Error("begin & end are not supported with match")
- ;e.begin=e.match,delete e.match}}function P(e,t){
- void 0===e.relevance&&(e.relevance=1)}const D=(e,t)=>{if(!e.beforeMatch)return
- ;if(e.starts)throw Error("beforeMatch cannot be used with starts")
- ;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t]
- })),e.keywords=n.keywords,e.begin=h(n.beforeMatch,g(n.begin)),e.starts={
- relevance:0,contains:[Object.assign(n,{endsParent:!0})]
- },e.relevance=0,delete n.beforeMatch
- },H=["of","and","for","in","not","or","if","then","parent","list","value"],C="keyword"
- ;function $(e,t,n=C){const i=Object.create(null)
- ;return"string"==typeof e?s(n,e.split(" ")):Array.isArray(e)?s(n,e):Object.keys(e).forEach((n=>{
- Object.assign(i,$(e[n],t,n))})),i;function s(e,n){
- t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|")
- ;i[n[0]]=[e,U(n[0],n[1])]}))}}function U(e,t){
- return t?Number(t):(e=>H.includes(e.toLowerCase()))(e)?0:1}const z={},W=e=>{
- console.error(e)},X=(e,...t)=>{console.log("WARN: "+e,...t)},G=(e,t)=>{
- z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0)
- },K=Error();function F(e,t,{key:n}){let i=0;const s=e[n],o={},r={}
- ;for(let e=1;e<=t.length;e++)r[e+i]=s[e],o[e+i]=!0,i+=p(t[e-1])
- ;e[n]=r,e[n]._emit=o,e[n]._multi=!0}function Z(e){(e=>{
- e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope,
- delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={
- _wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope
- }),(e=>{if(Array.isArray(e.begin)){
- if(e.skip||e.excludeBegin||e.returnBegin)throw W("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),
- K
- ;if("object"!=typeof e.beginScope||null===e.beginScope)throw W("beginScope must be object"),
- K;F(e,e.begin,{key:"beginScope"}),e.begin=m(e.begin,{joinWith:""})}})(e),(e=>{
- if(Array.isArray(e.end)){
- if(e.skip||e.excludeEnd||e.returnEnd)throw W("skip, excludeEnd, returnEnd not compatible with endScope: {}"),
- K
- ;if("object"!=typeof e.endScope||null===e.endScope)throw W("endScope must be object"),
- K;F(e,e.end,{key:"endScope"}),e.end=m(e.end,{joinWith:""})}})(e)}function V(e){
- function t(t,n){
- return RegExp(l(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":""))
- }class n{constructor(){
- this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}
- addRule(e,t){
- t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]),
- this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null)
- ;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(m(e,{joinWith:"|"
- }),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex
- ;const t=this.matcherRe.exec(e);if(!t)return null
- ;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n]
- ;return t.splice(0,n),Object.assign(t,i)}}class s{constructor(){
- this.rules=[],this.multiRegexes=[],
- this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){
- if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n
- ;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))),
- t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){
- return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){
- this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){
- const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex
- ;let n=t.exec(e)
- ;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{
- const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)}
- return n&&(this.regexIndex+=n.position+1,
- this.regexIndex===this.count&&this.considerAll()),n}}
- if(e.compilerExtensions||(e.compilerExtensions=[]),
- e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.")
- ;return e.classNameAliases=i(e.classNameAliases||{}),function n(o,r){const a=o
- ;if(o.isCompiled)return a
- ;[I,B,Z,D].forEach((e=>e(o,r))),e.compilerExtensions.forEach((e=>e(o,r))),
- o.__beforeBegin=null,[T,L,P].forEach((e=>e(o,r))),o.isCompiled=!0;let c=null
- ;return"object"==typeof o.keywords&&o.keywords.$pattern&&(o.keywords=Object.assign({},o.keywords),
- c=o.keywords.$pattern,
- delete o.keywords.$pattern),c=c||/\w+/,o.keywords&&(o.keywords=$(o.keywords,e.case_insensitive)),
- a.keywordPatternRe=t(c,!0),
- r&&(o.begin||(o.begin=/\B|\b/),a.beginRe=t(a.begin),o.end||o.endsWithParent||(o.end=/\B|\b/),
- o.end&&(a.endRe=t(a.end)),
- a.terminatorEnd=l(a.end)||"",o.endsWithParent&&r.terminatorEnd&&(a.terminatorEnd+=(o.end?"|":"")+r.terminatorEnd)),
- o.illegal&&(a.illegalRe=t(o.illegal)),
- o.contains||(o.contains=[]),o.contains=[].concat(...o.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>i(e,{
- variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?i(e,{
- starts:e.starts?i(e.starts):null
- }):Object.isFrozen(e)?i(e):e))("self"===e?o:e)))),o.contains.forEach((e=>{n(e,a)
- })),o.starts&&n(o.starts,r),a.matcher=(e=>{const t=new s
- ;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin"
- }))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end"
- }),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function q(e){
- return!!e&&(e.endsWithParent||q(e.starts))}class J extends Error{
- constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}}
- const Y=n,Q=i,ee=Symbol("nomatch"),te=n=>{
- const i=Object.create(null),s=Object.create(null),o=[];let r=!0
- ;const a="Could not find the language '{}', did you forget to load/include a language module?",l={
- disableAutodetect:!0,name:"Plain text",contains:[]};let p={
- ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,
- languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",
- cssSelector:"pre code",languages:null,__emitter:c};function b(e){
- return p.noHighlightRe.test(e)}function m(e,t,n){let i="",s=""
- ;"object"==typeof t?(i=e,
- n=t.ignoreIllegals,s=t.language):(G("10.7.0","highlight(lang, code, ...args) has been deprecated."),
- G("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),
- s=e,i=t),void 0===n&&(n=!0);const o={code:i,language:s};N("before:highlight",o)
- ;const r=o.result?o.result:E(o.language,o.code,n)
- ;return r.code=o.code,N("after:highlight",r),r}function E(e,n,s,o){
- const c=Object.create(null);function l(){if(!N.keywords)return void M.addText(R)
- ;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(R),n=""
- ;for(;t;){n+=R.substring(e,t.index)
- ;const s=_.case_insensitive?t[0].toLowerCase():t[0],o=(i=s,N.keywords[i]);if(o){
- const[e,i]=o
- ;if(M.addText(n),n="",c[s]=(c[s]||0)+1,c[s]<=7&&(j+=i),e.startsWith("_"))n+=t[0];else{
- const n=_.classNameAliases[e]||e;u(t[0],n)}}else n+=t[0]
- ;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(R)}var i
- ;n+=R.substring(e),M.addText(n)}function g(){null!=N.subLanguage?(()=>{
- if(""===R)return;let e=null;if("string"==typeof N.subLanguage){
- if(!i[N.subLanguage])return void M.addText(R)
- ;e=E(N.subLanguage,R,!0,S[N.subLanguage]),S[N.subLanguage]=e._top
- }else e=x(R,N.subLanguage.length?N.subLanguage:null)
- ;N.relevance>0&&(j+=e.relevance),M.__addSublanguage(e._emitter,e.language)
- })():l(),R=""}function u(e,t){
- ""!==e&&(M.startScope(t),M.addText(e),M.endScope())}function d(e,t){let n=1
- ;const i=t.length-1;for(;n<=i;){if(!e._emit[n]){n++;continue}
- const i=_.classNameAliases[e[n]]||e[n],s=t[n];i?u(s,i):(R=s,l(),R=""),n++}}
- function h(e,t){
- return e.scope&&"string"==typeof e.scope&&M.openNode(_.classNameAliases[e.scope]||e.scope),
- e.beginScope&&(e.beginScope._wrap?(u(R,_.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap),
- R=""):e.beginScope._multi&&(d(e.beginScope,t),R="")),N=Object.create(e,{parent:{
- value:N}}),N}function f(e,n,i){let s=((e,t)=>{const n=e&&e.exec(t)
- ;return n&&0===n.index})(e.endRe,i);if(s){if(e["on:end"]){const i=new t(e)
- ;e["on:end"](n,i),i.isMatchIgnored&&(s=!1)}if(s){
- for(;e.endsParent&&e.parent;)e=e.parent;return e}}
- if(e.endsWithParent)return f(e.parent,n,i)}function b(e){
- return 0===N.matcher.regexIndex?(R+=e[0],1):(T=!0,0)}function m(e){
- const t=e[0],i=n.substring(e.index),s=f(N,e,i);if(!s)return ee;const o=N
- ;N.endScope&&N.endScope._wrap?(g(),
- u(t,N.endScope._wrap)):N.endScope&&N.endScope._multi?(g(),
- d(N.endScope,e)):o.skip?R+=t:(o.returnEnd||o.excludeEnd||(R+=t),
- g(),o.excludeEnd&&(R=t));do{
- N.scope&&M.closeNode(),N.skip||N.subLanguage||(j+=N.relevance),N=N.parent
- }while(N!==s.parent);return s.starts&&h(s.starts,e),o.returnEnd?0:t.length}
- let w={};function y(i,o){const a=o&&o[0];if(R+=i,null==a)return g(),0
- ;if("begin"===w.type&&"end"===o.type&&w.index===o.index&&""===a){
- if(R+=n.slice(o.index,o.index+1),!r){const t=Error(`0 width match regex (${e})`)
- ;throw t.languageName=e,t.badRule=w.rule,t}return 1}
- if(w=o,"begin"===o.type)return(e=>{
- const n=e[0],i=e.rule,s=new t(i),o=[i.__beforeBegin,i["on:begin"]]
- ;for(const t of o)if(t&&(t(e,s),s.isMatchIgnored))return b(n)
- ;return i.skip?R+=n:(i.excludeBegin&&(R+=n),
- g(),i.returnBegin||i.excludeBegin||(R=n)),h(i,e),i.returnBegin?0:n.length})(o)
- ;if("illegal"===o.type&&!s){
- const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||"")+'"')
- ;throw e.mode=N,e}if("end"===o.type){const e=m(o);if(e!==ee)return e}
- if("illegal"===o.type&&""===a)return 1
- ;if(I>1e5&&I>3*o.index)throw Error("potential infinite loop, way more iterations than matches")
- ;return R+=a,a.length}const _=O(e)
- ;if(!_)throw W(a.replace("{}",e)),Error('Unknown language: "'+e+'"')
- ;const v=V(_);let k="",N=o||v;const S={},M=new p.__emitter(p);(()=>{const e=[]
- ;for(let t=N;t!==_;t=t.parent)t.scope&&e.unshift(t.scope)
- ;e.forEach((e=>M.openNode(e)))})();let R="",j=0,A=0,I=0,T=!1;try{
- if(_.__emitTokens)_.__emitTokens(n,M);else{for(N.matcher.considerAll();;){
- I++,T?T=!1:N.matcher.considerAll(),N.matcher.lastIndex=A
- ;const e=N.matcher.exec(n);if(!e)break;const t=y(n.substring(A,e.index),e)
- ;A=e.index+t}y(n.substring(A))}return M.finalize(),k=M.toHTML(),{language:e,
- value:k,relevance:j,illegal:!1,_emitter:M,_top:N}}catch(t){
- if(t.message&&t.message.includes("Illegal"))return{language:e,value:Y(n),
- illegal:!0,relevance:0,_illegalBy:{message:t.message,index:A,
- context:n.slice(A-100,A+100),mode:t.mode,resultSoFar:k},_emitter:M};if(r)return{
- language:e,value:Y(n),illegal:!1,relevance:0,errorRaised:t,_emitter:M,_top:N}
- ;throw t}}function x(e,t){t=t||p.languages||Object.keys(i);const n=(e=>{
- const t={value:Y(e),illegal:!1,relevance:0,_top:l,_emitter:new p.__emitter(p)}
- ;return t._emitter.addText(e),t})(e),s=t.filter(O).filter(k).map((t=>E(t,e,!1)))
- ;s.unshift(n);const o=s.sort(((e,t)=>{
- if(e.relevance!==t.relevance)return t.relevance-e.relevance
- ;if(e.language&&t.language){if(O(e.language).supersetOf===t.language)return 1
- ;if(O(t.language).supersetOf===e.language)return-1}return 0})),[r,a]=o,c=r
- ;return c.secondBest=a,c}function w(e){let t=null;const n=(e=>{
- let t=e.className+" ";t+=e.parentNode?e.parentNode.className:""
- ;const n=p.languageDetectRe.exec(t);if(n){const t=O(n[1])
- ;return t||(X(a.replace("{}",n[1])),
- X("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"}
- return t.split(/\s+/).find((e=>b(e)||O(e)))})(e);if(b(n))return
- ;if(N("before:highlightElement",{el:e,language:n
- }),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e)
- ;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),
- console.warn("https://github.com/highlightjs/highlight.js/wiki/security"),
- console.warn("The element with unescaped HTML:"),
- console.warn(e)),p.throwUnescapedHTML))throw new J("One of your code blocks includes unescaped HTML.",e.innerHTML)
- ;t=e;const i=t.textContent,o=n?m(i,{language:n,ignoreIllegals:!0}):x(i)
- ;e.innerHTML=o.value,e.dataset.highlighted="yes",((e,t,n)=>{const i=t&&s[t]||n
- ;e.classList.add("hljs"),e.classList.add("language-"+i)
- })(e,n,o.language),e.result={language:o.language,re:o.relevance,
- relevance:o.relevance},o.secondBest&&(e.secondBest={
- language:o.secondBest.language,relevance:o.secondBest.relevance
- }),N("after:highlightElement",{el:e,result:o,text:i})}let y=!1;function _(){
- "loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(w):y=!0
- }function O(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]}
- function v(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{
- s[e.toLowerCase()]=t}))}function k(e){const t=O(e)
- ;return t&&!t.disableAutodetect}function N(e,t){const n=e;o.forEach((e=>{
- e[n]&&e[n](t)}))}
- "undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{
- y&&_()}),!1),Object.assign(n,{highlight:m,highlightAuto:x,highlightAll:_,
- highlightElement:w,
- highlightBlock:e=>(G("10.7.0","highlightBlock will be removed entirely in v12.0"),
- G("10.7.0","Please use highlightElement now."),w(e)),configure:e=>{p=Q(p,e)},
- initHighlighting:()=>{
- _(),G("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")},
- initHighlightingOnLoad:()=>{
- _(),G("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.")
- },registerLanguage:(e,t)=>{let s=null;try{s=t(n)}catch(t){
- if(W("Language definition for '{}' could not be registered.".replace("{}",e)),
- !r)throw t;W(t),s=l}
- s.name||(s.name=e),i[e]=s,s.rawDefinition=t.bind(null,n),s.aliases&&v(s.aliases,{
- languageName:e})},unregisterLanguage:e=>{delete i[e]
- ;for(const t of Object.keys(s))s[t]===e&&delete s[t]},
- listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:v,
- autoDetection:k,inherit:Q,addPlugin:e=>{(e=>{
- e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{
- e["before:highlightBlock"](Object.assign({block:t.el},t))
- }),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{
- e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),o.push(e)},
- removePlugin:e=>{const t=o.indexOf(e);-1!==t&&o.splice(t,1)}}),n.debugMode=()=>{
- r=!1},n.safeMode=()=>{r=!0},n.versionString="11.9.0",n.regex={concat:h,
- lookahead:g,either:f,optional:d,anyNumberOfTimes:u}
- ;for(const t in j)"object"==typeof j[t]&&e(j[t]);return Object.assign(n,j),n
- },ne=te({});return ne.newInstance=()=>te({}),ne}()
- ;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);/*! `go` grammar compiled for Highlight.js 11.9.0 */
- (()=>{var e=(()=>{"use strict";return e=>{const n={
- keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"],
- type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"],
- literal:["true","false","iota","nil"],
- built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"]
- };return{name:"Go",aliases:["golang"],keywords:n,illegal:"",
- contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"string",
- variants:[e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,{begin:"`",end:"`"}]},{
- className:"number",variants:[{begin:e.C_NUMBER_RE+"[i]",relevance:1
- },e.C_NUMBER_MODE]},{begin:/:=/},{className:"function",beginKeywords:"func",
- end:"\\s*(\\{|$)",excludeEnd:!0,contains:[e.TITLE_MODE,{className:"params",
- begin:/\(/,end:/\)/,endsParent:!0,keywords:n,illegal:/["']/}]}]}}})()
- ;hljs.registerLanguage("go",e)})();/*! `json` grammar compiled for Highlight.js 11.9.0 */
- (()=>{var e=(()=>{"use strict";return e=>{const a=["true","false","null"],n={
- scope:"literal",beginKeywords:a.join(" ")};return{name:"JSON",keywords:{
- literal:a},contains:[{className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,
- relevance:1.01},{match:/[{}[\],:]/,className:"punctuation",relevance:0
- },e.QUOTE_STRING_MODE,n,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE],
- illegal:"\\S"}}})();hljs.registerLanguage("json",e)})();/*! `plaintext` grammar compiled for Highlight.js 11.9.0 */
- (()=>{var t=(()=>{"use strict";return t=>({name:"Plain text",
- aliases:["text","txt"],disableAutodetect:!0})})()
- ;hljs.registerLanguage("plaintext",t)})();
\ No newline at end of file
diff --git a/gno.land/pkg/gnoweb/static/js/marked.min.js b/gno.land/pkg/gnoweb/static/js/marked.min.js
deleted file mode 100644
index 3cc149db48e..00000000000
--- a/gno.land/pkg/gnoweb/static/js/marked.min.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * marked v12.0.2 - a markdown parser
- * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed)
- * https://github.com/markedjs/marked
- */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&","<":"<",">":">",'"':""","'":"'"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function p(e){return e.replace(h,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const u=/(^|[^\[])\^/g;function k(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(u,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function g(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}const f={exec:()=>null};function d(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:x(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=x(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=t[0].replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,"\n $1");e=x(e.replace(/^ *>[ \t]?/gm,""),"\n");const n=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=n,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l="",o="",a=!1;for(;e;){let n=!1;if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;l=t[0],e=e.substring(l.length);let s=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=0;this.options.pedantic?(h=2,o=s.trimStart()):(h=t[2].search(/[^ ]/),h=h>4?1:h,o=s.slice(h),h+=t[1].length);let p=!1;if(!s&&/^ *$/.test(c)&&(l+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,h-1)}}#`);for(;e;){const a=e.split("\n",1)[0];if(c=a,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=h||!c.trim())o+="\n"+c.slice(h);else{if(p)break;if(s.search(/[^ ]/)>=4)break;if(r.test(s))break;if(i.test(s))break;if(n.test(s))break;o+="\n"+c}p||c.trim()||(p=!0),l+=a+"\n",e=e.substring(a.length+1),s=c.slice(h)}}r.loose||(a?r.loose=!0:/\n *\n *$/.test(l)&&(a=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:l,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=l}r.items[r.items.length-1].raw=l.trimEnd(),r.items[r.items.length-1].text=o.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=d(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(const e of n)i.header.push({text:e,tokens:this.lexer.inline(e)});for(const e of r)i.rows.push(d(e,i.header.length).map((e=>({text:e,tokens:this.lexer.inline(e)}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=x(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),b(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return b(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const m=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,y=/(?:[*+-]|\d{1,9}[.)])/,$=k(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,y).replace(/blockCode/g,/ {4}/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),z=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,T=/(?!\s*\])(?:\\.|[^\[\]\\])+/,R=k(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",T).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),_=k(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,y).getRegex(),A="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",S=/|$))/,I=k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",S).replace("tag",A).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),E=k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),q={blockquote:k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",E).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:R,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:m,html:I,lheading:$,list:_,newline:/^(?: *(?:\n|$))+/,paragraph:E,table:f,text:/^[^\n]+/},Z=k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),L={...q,table:Z,paragraph:k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",Z).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex()},P={...q,html:k("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?\\1> *(?:\\n{2,}|\\s*$)| \\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",S).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:f,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:k(z).replace("hr",m).replace("heading"," *#{1,6} *[^\n]").replace("lheading",$).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Q=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,v=/^( {2,}|\\)\n(?!\s*$)/,B="\\p{P}\\p{S}",C=k(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,B).getRegex(),M=k(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,B).getRegex(),O=k("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,B).getRegex(),D=k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,B).getRegex(),j=k(/\\([punct])/,"gu").replace(/punct/g,B).getRegex(),H=k(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),U=k(S).replace("(?:--\x3e|$)","--\x3e").getRegex(),X=k("^comment|^[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",U).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),F=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,N=k(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",F).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),G=k(/^!?\[(label)\]\[(ref)\]/).replace("label",F).replace("ref",T).getRegex(),J=k(/^!?\[(ref)\](?:\[\])?/).replace("ref",T).getRegex(),K={_backpedal:f,anyPunctuation:j,autolink:H,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:v,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:f,emStrongLDelim:M,emStrongRDelimAst:O,emStrongRDelimUnd:D,escape:Q,link:N,nolink:J,punctuation:C,reflink:G,reflinkSearch:k("reflink|nolink(?!\\()","g").replace("reflink",G).replace("nolink",J).getRegex(),tag:X,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\t+" ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.space(e))e=e.substring(n.raw.length),1===n.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(n);else if(n=this.tokenizer.code(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(n):(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.fences(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.heading(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.hr(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.blockquote(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.list(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.html(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.def(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[n.tag]||(this.tokens.links[n.tag]={href:n.href,title:n.title}):(s.raw+="\n"+n.raw,s.text+="\n"+n.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.table(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.lheading(e))e=e.substring(n.raw.length),t.push(n);else{if(r=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(n=this.tokenizer.paragraph(r)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n),i=r.length!==e.length,e=e.substring(n.raw.length);else if(n=this.tokenizer.text(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class se{options;constructor(t){this.options=t||e.defaults}code(e,t,n){const s=(t||"").match(/^\S*/)?.[0];return e=e.replace(/\n$/,"")+"\n",s?''+(n?e:c(e,!0))+"
\n":""+(n?e:c(e,!0))+"
\n"}blockquote(e){return`\n${e} \n`}html(e,t){return e}heading(e,t,n){return`${e} \n`}hr(){return" \n"}list(e,t,n){const s=t?"ol":"ul";return"<"+s+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+""+s+">\n"}listitem(e,t,n){return`${e} \n`}checkbox(e){return" '}paragraph(e){return`${e}
\n`}table(e,t){return t&&(t=`${t} `),"\n"}tablerow(e){return`\n${e} \n`}tablecell(e,t){const n=t.header?"th":"td";return(t.align?`<${n} align="${t.align}">`:`<${n}>`)+e+`${n}>\n`}strong(e){return`${e} `}em(e){return`${e} `}codespan(e){return`${e}
`}br(){return" "}del(e){return`${e}`}link(e,t,n){const s=g(e);if(null===s)return n;let r='"+n+" ",r}image(e,t,n){const s=g(e);if(null===s)return n;let r=` ",r}text(e){return e}}class re{strong(e){return e}em(e){return e}codespan(e){return e}del(e){return e}html(e){return e}text(e){return e}link(e,t,n){return""+n}image(e,t,n){return""+n}br(){return""}}class ie{options;renderer;textRenderer;constructor(t){this.options=t||e.defaults,this.options.renderer=this.options.renderer||new se,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new re}static parse(e,t){return new ie(t).parse(e)}static parseInline(e,t){return new ie(t).parseInline(e)}parse(e,t=!0){let n="";for(let s=0;s0&&"paragraph"===n.tokens[0].type?(n.tokens[0].text=e+" "+n.tokens[0].text,n.tokens[0].tokens&&n.tokens[0].tokens.length>0&&"text"===n.tokens[0].tokens[0].type&&(n.tokens[0].tokens[0].text=e+" "+n.tokens[0].tokens[0].text)):n.tokens.unshift({type:"text",text:e+" "}):o+=e+" "}o+=this.parse(n.tokens,i),l+=this.renderer.listitem(o,r,!!s)}n+=this.renderer.list(l,t,s);continue}case"html":{const e=r;n+=this.renderer.html(e.text,e.block);continue}case"paragraph":{const e=r;n+=this.renderer.paragraph(this.parseInline(e.tokens));continue}case"text":{let i=r,l=i.tokens?this.parseInline(i.tokens):i.text;for(;s+1{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new se(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new w(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new le;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.hooks[s],i=t[s];le.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ne.lex(e,t??this.defaults)}parser(e,t){return ie.parse(e,t??this.defaults)}#e(e,t){return(n,s)=>{const r={...s},i={...this.defaults,...r};!0===this.defaults.async&&!1===r.async&&(i.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),i.async=!0);const l=this.#t(!!i.silent,!!i.async);if(null==n)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof n)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i),i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(n):n).then((t=>e(t,i))).then((e=>i.hooks?i.hooks.processAllTokens(e):e)).then((e=>i.walkTokens?Promise.all(this.walkTokens(e,i.walkTokens)).then((()=>e)):e)).then((e=>t(e,i))).then((e=>i.hooks?i.hooks.postprocess(e):e)).catch(l);try{i.hooks&&(n=i.hooks.preprocess(n));let s=e(n,i);i.hooks&&(s=i.hooks.processAllTokens(s)),i.walkTokens&&this.walkTokens(s,i.walkTokens);let r=t(s,i);return i.hooks&&(r=i.hooks.postprocess(r)),r}catch(e){return l(e)}}}#t(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="An error occurred:
"+c(n.message+"",!0)+" ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const ae=new oe;function ce(e,t){return ae.parse(e,t)}ce.options=ce.setOptions=function(e){return ae.setOptions(e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.getDefaults=t,ce.defaults=e.defaults,ce.use=function(...e){return ae.use(...e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.walkTokens=function(e,t){return ae.walkTokens(e,t)},ce.parseInline=ae.parseInline,ce.Parser=ie,ce.parser=ie.parse,ce.Renderer=se,ce.TextRenderer=re,ce.Lexer=ne,ce.lexer=ne.lex,ce.Tokenizer=w,ce.Hooks=le,ce.parse=ce;const he=ce.options,pe=ce.setOptions,ue=ce.use,ke=ce.walkTokens,ge=ce.parseInline,fe=ce,de=ie.parse,xe=ne.lex;e.Hooks=le,e.Lexer=ne,e.Marked=oe,e.Parser=ie,e.Renderer=se,e.TextRenderer=re,e.Tokenizer=w,e.getDefaults=t,e.lexer=xe,e.marked=ce,e.options=he,e.parse=fe,e.parseInline=ge,e.parser=de,e.setOptions=pe,e.use=ue,e.walkTokens=ke}));
-/**
- * Minified by jsDelivr using Terser v5.19.2.
- * Original file: /npm/marked-highlight@2.1.1/lib/index.umd.js
- *
- * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
- */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).markedHighlight={})}(this,(function(e){"use strict";function t(e){return(e||"").match(/\S*/)[0]}function n(e){return t=>{"string"==typeof t&&t!==e.text&&(e.escaped=!0,e.text=t)}}const i=/[&<>"']/,o=new RegExp(i.source,"g"),r=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,g=new RegExp(r.source,"g"),h={"&":"&","<":"<",">":">",'"':""","'":"'"},s=e=>h[e];function c(e,t){if(t){if(i.test(e))return e.replace(o,s)}else if(r.test(e))return e.replace(g,s);return e}e.markedHighlight=function(e){if("function"==typeof e&&(e={highlight:e}),!e||"function"!=typeof e.highlight)throw new Error("Must provide highlight function");return"string"!=typeof e.langPrefix&&(e.langPrefix="language-"),{async:!!e.async,walkTokens(i){if("code"!==i.type)return;const o=t(i.lang);if(e.async)return Promise.resolve(e.highlight(i.text,o,i.lang||"")).then(n(i));const r=e.highlight(i.text,o,i.lang||"");if(r instanceof Promise)throw new Error("markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.");n(i)(r)},renderer:{code(n,i,o){const r=t(i),g=r?` class="${e.langPrefix}${c(r)}"`:"";return n=n.replace(/\n$/,""),`${o?n:c(n,!0)}\n
`}}}}}));
-//# sourceMappingURL=/sm/3bfb625a4ed441ddc1f215743851a4b727156eef53b458bd31c51a627ce891c9.map
\ No newline at end of file
diff --git a/gno.land/pkg/gnoweb/static/js/purify.min.js b/gno.land/pkg/gnoweb/static/js/purify.min.js
deleted file mode 100644
index ed613fcc36f..00000000000
--- a/gno.land/pkg/gnoweb/static/js/purify.min.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/*! @license DOMPurify 2.3.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.6/LICENSE */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).DOMPurify=t()}(this,(function(){"use strict";var e=Object.hasOwnProperty,t=Object.setPrototypeOf,n=Object.isFrozen,r=Object.getPrototypeOf,o=Object.getOwnPropertyDescriptor,i=Object.freeze,a=Object.seal,l=Object.create,c="undefined"!=typeof Reflect&&Reflect,s=c.apply,u=c.construct;s||(s=function(e,t,n){return e.apply(t,n)}),i||(i=function(e){return e}),a||(a=function(e){return e}),u||(u=function(e,t){return new(Function.prototype.bind.apply(e,[null].concat(function(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t1?n-1:0),o=1;o/gm),z=a(/^data-[\-\w.\u00B7-\uFFFF]/),B=a(/^aria-[\-\w]+$/),P=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),j=a(/^(?:\w+script|data):/i),G=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),W=a(/^html$/i),q="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};function Y(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t0&&void 0!==arguments[0]?arguments[0]:K(),n=function(t){return e(t)};if(n.version="2.3.6",n.removed=[],!t||!t.document||9!==t.document.nodeType)return n.isSupported=!1,n;var r=t.document,o=t.document,a=t.DocumentFragment,l=t.HTMLTemplateElement,c=t.Node,s=t.Element,u=t.NodeFilter,m=t.NamedNodeMap,A=void 0===m?t.NamedNodeMap||t.MozNamedAttrMap:m,$=t.HTMLFormElement,X=t.DOMParser,Z=t.trustedTypes,J=s.prototype,Q=w(J,"cloneNode"),ee=w(J,"nextSibling"),te=w(J,"childNodes"),ne=w(J,"parentNode");if("function"==typeof l){var re=o.createElement("template");re.content&&re.content.ownerDocument&&(o=re.content.ownerDocument)}var oe=V(Z,r),ie=oe?oe.createHTML(""):"",ae=o,le=ae.implementation,ce=ae.createNodeIterator,se=ae.createDocumentFragment,ue=ae.getElementsByTagName,me=r.importNode,fe={};try{fe=x(o).documentMode?o.documentMode:{}}catch(e){}var de={};n.isSupported="function"==typeof ne&&le&&void 0!==le.createHTMLDocument&&9!==fe;var pe=H,he=U,ge=z,ye=B,ve=j,be=G,Te=P,Ne=null,Ae=E({},[].concat(Y(k),Y(S),Y(_),Y(O),Y(M))),Ee=null,xe=E({},[].concat(Y(L),Y(R),Y(I),Y(F))),we=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ke=null,Se=null,_e=!0,De=!0,Oe=!1,Ce=!1,Me=!1,Le=!1,Re=!1,Ie=!1,Fe=!1,He=!1,Ue=!0,ze=!0,Be=!1,Pe={},je=null,Ge=E({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),We=null,qe=E({},["audio","video","img","source","image","track"]),Ye=null,Ke=E({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ve="http://www.w3.org/1998/Math/MathML",$e="http://www.w3.org/2000/svg",Xe="http://www.w3.org/1999/xhtml",Ze=Xe,Je=!1,Qe=void 0,et=["application/xhtml+xml","text/html"],tt="text/html",nt=void 0,rt=null,ot=o.createElement("form"),it=function(e){return e instanceof RegExp||e instanceof Function},at=function(e){rt&&rt===e||(e&&"object"===(void 0===e?"undefined":q(e))||(e={}),e=x(e),Ne="ALLOWED_TAGS"in e?E({},e.ALLOWED_TAGS):Ae,Ee="ALLOWED_ATTR"in e?E({},e.ALLOWED_ATTR):xe,Ye="ADD_URI_SAFE_ATTR"in e?E(x(Ke),e.ADD_URI_SAFE_ATTR):Ke,We="ADD_DATA_URI_TAGS"in e?E(x(qe),e.ADD_DATA_URI_TAGS):qe,je="FORBID_CONTENTS"in e?E({},e.FORBID_CONTENTS):Ge,ke="FORBID_TAGS"in e?E({},e.FORBID_TAGS):{},Se="FORBID_ATTR"in e?E({},e.FORBID_ATTR):{},Pe="USE_PROFILES"in e&&e.USE_PROFILES,_e=!1!==e.ALLOW_ARIA_ATTR,De=!1!==e.ALLOW_DATA_ATTR,Oe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Ce=e.SAFE_FOR_TEMPLATES||!1,Me=e.WHOLE_DOCUMENT||!1,Ie=e.RETURN_DOM||!1,Fe=e.RETURN_DOM_FRAGMENT||!1,He=e.RETURN_TRUSTED_TYPE||!1,Re=e.FORCE_BODY||!1,Ue=!1!==e.SANITIZE_DOM,ze=!1!==e.KEEP_CONTENT,Be=e.IN_PLACE||!1,Te=e.ALLOWED_URI_REGEXP||Te,Ze=e.NAMESPACE||Xe,e.CUSTOM_ELEMENT_HANDLING&&it(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(we.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&it(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(we.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(we.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Qe=Qe=-1===et.indexOf(e.PARSER_MEDIA_TYPE)?tt:e.PARSER_MEDIA_TYPE,nt="application/xhtml+xml"===Qe?function(e){return e}:h,Ce&&(De=!1),Fe&&(Ie=!0),Pe&&(Ne=E({},[].concat(Y(M))),Ee=[],!0===Pe.html&&(E(Ne,k),E(Ee,L)),!0===Pe.svg&&(E(Ne,S),E(Ee,R),E(Ee,F)),!0===Pe.svgFilters&&(E(Ne,_),E(Ee,R),E(Ee,F)),!0===Pe.mathMl&&(E(Ne,O),E(Ee,I),E(Ee,F))),e.ADD_TAGS&&(Ne===Ae&&(Ne=x(Ne)),E(Ne,e.ADD_TAGS)),e.ADD_ATTR&&(Ee===xe&&(Ee=x(Ee)),E(Ee,e.ADD_ATTR)),e.ADD_URI_SAFE_ATTR&&E(Ye,e.ADD_URI_SAFE_ATTR),e.FORBID_CONTENTS&&(je===Ge&&(je=x(je)),E(je,e.FORBID_CONTENTS)),ze&&(Ne["#text"]=!0),Me&&E(Ne,["html","head","body"]),Ne.table&&(E(Ne,["tbody"]),delete ke.tbody),i&&i(e),rt=e)},lt=E({},["mi","mo","mn","ms","mtext"]),ct=E({},["foreignobject","desc","title","annotation-xml"]),st=E({},S);E(st,_),E(st,D);var ut=E({},O);E(ut,C);var mt=function(e){var t=ne(e);t&&t.tagName||(t={namespaceURI:Xe,tagName:"template"});var n=h(e.tagName),r=h(t.tagName);if(e.namespaceURI===$e)return t.namespaceURI===Xe?"svg"===n:t.namespaceURI===Ve?"svg"===n&&("annotation-xml"===r||lt[r]):Boolean(st[n]);if(e.namespaceURI===Ve)return t.namespaceURI===Xe?"math"===n:t.namespaceURI===$e?"math"===n&&ct[r]:Boolean(ut[n]);if(e.namespaceURI===Xe){if(t.namespaceURI===$e&&!ct[r])return!1;if(t.namespaceURI===Ve&&!lt[r])return!1;var o=E({},["title","style","font","a","script"]);return!ut[n]&&(o[n]||!st[n])}return!1},ft=function(e){p(n.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=ie}catch(t){e.remove()}}},dt=function(e,t){try{p(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Ee[e])if(Ie||Fe)try{ft(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},pt=function(e){var t=void 0,n=void 0;if(Re)e=" "+e;else{var r=g(e,/^[\r\n\t ]+/);n=r&&r[0]}"application/xhtml+xml"===Qe&&(e=''+e+"");var i=oe?oe.createHTML(e):e;if(Ze===Xe)try{t=(new X).parseFromString(i,Qe)}catch(e){}if(!t||!t.documentElement){t=le.createDocument(Ze,"template",null);try{t.documentElement.innerHTML=Je?"":i}catch(e){}}var a=t.body||t.documentElement;return e&&n&&a.insertBefore(o.createTextNode(n),a.childNodes[0]||null),Ze===Xe?ue.call(t,Me?"html":"body")[0]:Me?t.documentElement:a},ht=function(e){return ce.call(e.ownerDocument||e,e,u.SHOW_ELEMENT|u.SHOW_COMMENT|u.SHOW_TEXT,null,!1)},gt=function(e){return e instanceof $&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof A)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore)},yt=function(e){return"object"===(void 0===c?"undefined":q(c))?e instanceof c:e&&"object"===(void 0===e?"undefined":q(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},vt=function(e,t,r){de[e]&&f(de[e],(function(e){e.call(n,t,r,rt)}))},bt=function(e){var t=void 0;if(vt("beforeSanitizeElements",e,null),gt(e))return ft(e),!0;if(g(e.nodeName,/[\u0080-\uFFFF]/))return ft(e),!0;var r=nt(e.nodeName);if(vt("uponSanitizeElement",e,{tagName:r,allowedTags:Ne}),!yt(e.firstElementChild)&&(!yt(e.content)||!yt(e.content.firstElementChild))&&T(/<[/\w]/g,e.innerHTML)&&T(/<[/\w]/g,e.textContent))return ft(e),!0;if("select"===r&&T(/=0;--a)o.insertBefore(Q(i[a],!0),ee(e))}return ft(e),!0}return e instanceof s&&!mt(e)?(ft(e),!0):"noscript"!==r&&"noembed"!==r||!T(/<\/no(script|embed)/i,e.innerHTML)?(Ce&&3===e.nodeType&&(t=e.textContent,t=y(t,pe," "),t=y(t,he," "),e.textContent!==t&&(p(n.removed,{element:e.cloneNode()}),e.textContent=t)),vt("afterSanitizeElements",e,null),!1):(ft(e),!0)},Tt=function(e,t,n){if(Ue&&("id"===t||"name"===t)&&(n in o||n in ot))return!1;if(De&&!Se[t]&&T(ge,t));else if(_e&&T(ye,t));else if(!Ee[t]||Se[t]){if(!(Nt(e)&&(we.tagNameCheck instanceof RegExp&&T(we.tagNameCheck,e)||we.tagNameCheck instanceof Function&&we.tagNameCheck(e))&&(we.attributeNameCheck instanceof RegExp&&T(we.attributeNameCheck,t)||we.attributeNameCheck instanceof Function&&we.attributeNameCheck(t))||"is"===t&&we.allowCustomizedBuiltInElements&&(we.tagNameCheck instanceof RegExp&&T(we.tagNameCheck,n)||we.tagNameCheck instanceof Function&&we.tagNameCheck(n))))return!1}else if(Ye[t]);else if(T(Te,y(n,be,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==v(n,"data:")||!We[e]){if(Oe&&!T(ve,y(n,be,"")));else if(n)return!1}else;return!0},Nt=function(e){return e.indexOf("-")>0},At=function(e){var t=void 0,r=void 0,o=void 0,i=void 0;vt("beforeSanitizeAttributes",e,null);var a=e.attributes;if(a){var l={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Ee};for(i=a.length;i--;){var c=t=a[i],s=c.name,u=c.namespaceURI;if(r=b(t.value),o=nt(s),l.attrName=o,l.attrValue=r,l.keepAttr=!0,l.forceKeepAttr=void 0,vt("uponSanitizeAttribute",e,l),r=l.attrValue,!l.forceKeepAttr&&(dt(s,e),l.keepAttr))if(T(/\/>/i,r))dt(s,e);else{Ce&&(r=y(r,pe," "),r=y(r,he," "));var m=nt(e.nodeName);if(Tt(m,o,r))try{u?e.setAttributeNS(u,s,r):e.setAttribute(s,r),d(n.removed)}catch(e){}}}vt("afterSanitizeAttributes",e,null)}},Et=function e(t){var n=void 0,r=ht(t);for(vt("beforeSanitizeShadowDOM",t,null);n=r.nextNode();)vt("uponSanitizeShadowNode",n,null),bt(n)||(n.content instanceof a&&e(n.content),At(n));vt("afterSanitizeShadowDOM",t,null)};return n.sanitize=function(e,o){var i=void 0,l=void 0,s=void 0,u=void 0,m=void 0;if((Je=!e)&&(e="\x3c!--\x3e"),"string"!=typeof e&&!yt(e)){if("function"!=typeof e.toString)throw N("toString is not a function");if("string"!=typeof(e=e.toString()))throw N("dirty is not a string, aborting")}if(!n.isSupported){if("object"===q(t.toStaticHTML)||"function"==typeof t.toStaticHTML){if("string"==typeof e)return t.toStaticHTML(e);if(yt(e))return t.toStaticHTML(e.outerHTML)}return e}if(Le||at(o),n.removed=[],"string"==typeof e&&(Be=!1),Be){if(e.nodeName){var f=nt(e.nodeName);if(!Ne[f]||ke[f])throw N("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof c)1===(l=(i=pt("\x3c!----\x3e")).ownerDocument.importNode(e,!0)).nodeType&&"BODY"===l.nodeName||"HTML"===l.nodeName?i=l:i.appendChild(l);else{if(!Ie&&!Ce&&!Me&&-1===e.indexOf("<"))return oe&&He?oe.createHTML(e):e;if(!(i=pt(e)))return Ie?null:He?ie:""}i&&Re&&ft(i.firstChild);for(var d=ht(Be?e:i);s=d.nextNode();)3===s.nodeType&&s===u||bt(s)||(s.content instanceof a&&Et(s.content),At(s),u=s);if(u=null,Be)return e;if(Ie){if(Fe)for(m=se.call(i.ownerDocument);i.firstChild;)m.appendChild(i.firstChild);else m=i;return Ee.shadowroot&&(m=me.call(r,m,!0)),m}var p=Me?i.outerHTML:i.innerHTML;return Me&&Ne["!doctype"]&&i.ownerDocument&&i.ownerDocument.doctype&&i.ownerDocument.doctype.name&&T(W,i.ownerDocument.doctype.name)&&(p="\n"+p),Ce&&(p=y(p,pe," "),p=y(p,he," ")),oe&&He?oe.createHTML(p):p},n.setConfig=function(e){at(e),Le=!0},n.clearConfig=function(){rt=null,Le=!1},n.isValidAttribute=function(e,t,n){rt||at({});var r=nt(e),o=nt(t);return Tt(r,o,n)},n.addHook=function(e,t){"function"==typeof t&&(de[e]=de[e]||[],p(de[e],t))},n.removeHook=function(e){de[e]&&d(de[e])},n.removeHooks=function(e){de[e]&&(de[e]=[])},n.removeAllHooks=function(){de={}},n}()}));
-//# sourceMappingURL=purify.min.js.map
diff --git a/gno.land/pkg/gnoweb/static/js/realm_help.js b/gno.land/pkg/gnoweb/static/js/realm_help.js
deleted file mode 100644
index 30cfacd5f59..00000000000
--- a/gno.land/pkg/gnoweb/static/js/realm_help.js
+++ /dev/null
@@ -1,111 +0,0 @@
-function main() {
- // init
- var myAddr = getMyAddress()
- u("#my_address").first().value = myAddr;
- setMyAddress(myAddr);
- // main renders
- u("div.func_spec").each(function(x) {
- updateCommand(u(x));
- });
- // main hooks
- u("div.func_spec input").on("input", function(e) {
- var x = u(e.currentTarget).closest("div.func_spec");
- updateCommand(x);
- });
- // special case: when address changes.
- u("#my_address").on("input", function(e) {
- var value = u("#my_address").first().value;
- setMyAddress(value)
- u("div.func_spec").each(function(node, i) {
- updateCommand(u(node));
- });
- });
-};
-
-function setMyAddress(addr) {
- localStorage.setItem("my_address", addr);
-}
-
-function getMyAddress() {
- var myAddr = localStorage.getItem("my_address");
- if (!myAddr) {
- return "";
- }
- return myAddr;
-}
-
-// x: the u("div.func_spec") element.
-function updateCommand(x) {
- var realmPath = u("#data").data("realm-path");
- var remote = u("#data").data("remote");
- var chainid = u("#data").data("chainid");
- var funcName = x.data("func-name");
- var ins = x.find("table>tbody>tr.func_params input");
- var vals = [];
- ins.each(function(input) {
- vals.push(input.value);
- });
- var myAddr = getMyAddress() || "ADDRESS";
- var shell = x.find(".shell_command");
- shell.empty();
-
- // command Z: all in one.
- shell.append(u("").text("### INSECURE BUT QUICK ###")).append(u(" "));
- var args = ["gnokey", "maketx", "call",
- "-pkgpath", shq(realmPath), "-func", shq(funcName),
- "-gas-fee", "1000000ugnot", "-gas-wanted", "2000000",
- "-send", shq(""),
- "-broadcast", "-chainid", shq(chainid)];
- vals.forEach(function(arg) {
- args.push("-args");
- args.push(shq(arg));
- });
- args.push("-remote", shq(remote));
- args.push(myAddr);
- var command = args.join(" ");
- shell.append(u("").text(command)).append(u(" ")).append(u(" "));
-
- // or...
- shell.append(u("").text("### FULL SECURITY WITH AIRGAP ###")).append(u(" "));
-
- // command 0: query account info.
- var args = ["gnokey", "query", "-remote", shq(remote), "auth/accounts/" + myAddr];
- var command = args.join(" ");
- shell.append(u("").text(command)).append(u(" "));
-
- // command 1: construct tx.
- var args = ["gnokey", "maketx", "call",
- "-pkgpath", shq(realmPath), "-func", shq(funcName),
- "-gas-fee", "1000000ugnot", "-gas-wanted", "2000000",
- "-send", shq("")];
- vals.forEach(function(arg) {
- args.push("-args");
- args.push(shq(arg));
- });
- args.push(myAddr)
- var command = args.join(" ");
- command = command + " > call.tx";
- shell.append(u("").text(command)).append(u(" "));
-
- // command 2: sign tx.
- var args = ["gnokey", "sign",
- "-tx-path", "call.tx", "-chainid", shq(chainid),
- "-account-number", "ACCOUNTNUMBER",
- "-account-sequence", "SEQUENCENUMBER", myAddr];
- var command = args.join(" ");
- shell.append(u("").text(command)).append(u(" "));
-
- // command 3: broadcast tx.
- var args = ["gnokey", "broadcast", "-remote", shq(remote), "call.tx"];
- var command = args.join(" ");
- command = command;
- shell.append(u("").text(command)).append(u(" "));
-}
-
-// Jae: why isn't this a library somewhere?
-function shq(s) {
- var s2 = String(s).replace(/\t/g, '\\t');
- var s2 = String(s2).replace(/\n/g, '\\n');
- var s2 = String(s2).replace(/([$'"`\\!])/g, '\\$1');
- return '"' + s2 + '"';
-};
diff --git a/gno.land/pkg/gnoweb/static/js/renderer.js b/gno.land/pkg/gnoweb/static/js/renderer.js
deleted file mode 100644
index 0aa6400633d..00000000000
--- a/gno.land/pkg/gnoweb/static/js/renderer.js
+++ /dev/null
@@ -1,225 +0,0 @@
-/**
- * Replaces @username by [@username](/r/demo/users:username)
- * @param string rawData text to render usernames in
- * @returns string rendered text
- */
-function renderUsernames(raw) {
- return raw.replace(/( |\n)@([_a-z0-9]{5,16})/, "$1[@$2](/r/demo/users:$2)");
-}
-
-function parseContent(source, isCode) {
- if (isCode) {
- const highlightedCode = hljs.highlightAuto(source).value;
- const codeElement = document.createElement("code");
- codeElement.classList.add("hljs");
- codeElement.innerHTML = highlightedCode;
-
- const preElement = document.createElement("pre");
- preElement.appendChild(codeElement);
-
- return preElement;
- } else {
- const { markedHighlight } = globalThis.markedHighlight;
- const { Marked } = globalThis.marked;
- const markedInstance = new Marked(
- markedHighlight({
- langPrefix: "language-",
- highlight(code, lang, info) {
- if (lang === "json") {
- try {
- code = JSON.stringify(JSON.parse(code), null, 2);
- } catch {
- console.error('Error: The provided JSON code is invalid.');
- }
- }
- const language = hljs.getLanguage(lang) ? lang : "plaintext";
- return hljs.highlight(code, { language }).value;
- },
- })
- );
- markedInstance.setOptions({ gfm: true });
- const doc = new DOMParser().parseFromString(source, "text/html");
- const contents = doc.documentElement.textContent;
-
- return markedInstance.parse(contents);
- }
-}
-
-/*
- * ### ACCORDIONS ###
- *
- * This content is licensed according to the W3C Software License at
- * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
- *
- * Desc: Simple accordion pattern example
- */
-
-class Accordion {
- constructor(domNode) {
- this.buttonEl = domNode;
- this.contentEl = this.buttonEl.nextElementSibling ?? this.buttonEl.parentElement.nextElementSibling;
- this.open = this.buttonEl.getAttribute("aria-expanded") === "true";
-
- this.buttonEl.addEventListener("click", this.onButtonClick.bind(this));
- }
-
- onButtonClick() {
- this.toggle(!this.open);
- }
-
- toggle(open) {
- if (open === this.open) {
- return;
- }
-
- this.open = open;
-
- this.buttonEl.setAttribute("aria-expanded", `${open}`);
- if (open) {
- this.contentEl.classList.remove("is-hidden");
- } else {
- this.contentEl.classList.add("is-hidden");
- }
- }
-}
-
-/*
- * ### TABS ###
- *
- * This content is licensed according to the W3C Software License at
- * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
- *
- * Desc: Tablist widget that implements ARIA Authoring Practices
- */
-
-class Tabs {
- constructor(groupNode) {
- this.tablistNode = groupNode;
-
- this.tabs = [];
-
- this.firstTab = null;
- this.lastTab = null;
-
- this.tabs = Array.from(this.tablistNode.querySelectorAll("[role=tab]"));
- this.tabpanels = [];
-
- for (let tab of this.tabs) {
- const tabpanel = document.getElementById(tab.getAttribute("aria-controls"));
-
- tab.tabIndex = -1;
- tab.setAttribute("aria-selected", "false");
- this.tabpanels.push(tabpanel);
-
- tab.addEventListener("keydown", this.onKeydown.bind(this));
- tab.addEventListener("click", this.onClick.bind(this));
-
- if (!this.firstTab) {
- this.firstTab = tab;
- }
- this.lastTab = tab;
- }
-
- this.setSelectedTab(this.firstTab, false);
- }
-
- setSelectedTab(currentTab, setFocus) {
- if (typeof setFocus !== "boolean") {
- setFocus = true;
- }
- for (let i = 0; i < this.tabs.length; i += 1) {
- var tab = this.tabs[i];
- if (currentTab === tab) {
- tab.setAttribute("aria-selected", "true");
- tab.removeAttribute("tabindex");
- this.tabpanels[i].classList.remove("is-hidden");
- if (setFocus) {
- tab.focus();
- }
- } else {
- tab.setAttribute("aria-selected", "false");
- tab.tabIndex = -1;
- this.tabpanels[i].classList.add("is-hidden");
- }
- }
- }
-
- setSelectedToPreviousTab(currentTab) {
- let index;
-
- if (currentTab === this.firstTab) {
- this.setSelectedTab(this.lastTab);
- } else {
- index = this.tabs.indexOf(currentTab);
- this.setSelectedTab(this.tabs[index - 1]);
- }
- }
-
- setSelectedToNextTab(currentTab) {
- var index;
-
- if (currentTab === this.lastTab) {
- this.setSelectedTab(this.firstTab);
- } else {
- index = this.tabs.indexOf(currentTab);
- this.setSelectedTab(this.tabs[index + 1]);
- }
- }
-
- /* EVENT HANDLERS */
-
- onKeydown(event) {
- const tgt = event.currentTarget,
- flag = false;
-
- switch (event.key) {
- case "ArrowLeft":
- this.setSelectedToPreviousTab(tgt);
- flag = true;
- break;
-
- case "ArrowRight":
- this.setSelectedToNextTab(tgt);
- flag = true;
- break;
-
- case "Home":
- this.setSelectedTab(this.firstTab);
- flag = true;
- break;
-
- case "End":
- this.setSelectedTab(this.lastTab);
- flag = true;
- break;
-
- default:
- break;
- }
-
- if (flag) {
- event.stopPropagation();
- event.preventDefault();
- }
- }
-
- onClick(event) {
- this.setSelectedTab(event.currentTarget);
- }
-}
-
-/*
- * ### INIT COMPONENTS ###
- */
-
-window.addEventListener("load", function () {
- const accordions = Array.from(document.querySelectorAll(".accordion-trigger"));
- for (let accordion of accordions) {
- new Accordion(accordion);
- }
-
- const tablists = Array.from(document.querySelectorAll("[role=tablist].tabs"));
- for (let tab of tablists) {
- new Tabs(tab);
- }
-});
diff --git a/gno.land/pkg/gnoweb/static/js/umbrella.js b/gno.land/pkg/gnoweb/static/js/umbrella.js
deleted file mode 100644
index 9735d031265..00000000000
--- a/gno.land/pkg/gnoweb/static/js/umbrella.js
+++ /dev/null
@@ -1,807 +0,0 @@
-// Umbrella JS http://umbrellajs.com/
-// -----------
-// Small, lightweight jQuery alternative
-// @author Francisco Presencia Fandos https://francisco.io/
-// @inspiration http://youmightnotneedjquery.com/
-
-// Initialize the library
-var u = function (parameter, context) {
- // Make it an instance of u() to avoid needing 'new' as in 'new u()' and just
- // use 'u().bla();'.
- // @reference http://stackoverflow.com/q/24019863
- // @reference http://stackoverflow.com/q/8875878
- if (!(this instanceof u)) {
- return new u(parameter, context);
- }
-
- // No need to further processing it if it's already an instance
- if (parameter instanceof u) {
- return parameter;
- }
-
- // Parse it as a CSS selector if it's a string
- if (typeof parameter === 'string') {
- parameter = this.select(parameter, context);
- }
-
- // If we're referring a specific node as in on('click', function(){ u(this) })
- // or the select() function returned a single node such as in '#id'
- if (parameter && parameter.nodeName) {
- parameter = [parameter];
- }
-
- // Convert to an array, since there are many 'array-like' stuff in js-land
- this.nodes = this.slice(parameter);
-};
-
-// Map u(...).length to u(...).nodes.length
-u.prototype = {
- get length () {
- return this.nodes.length;
- }
-};
-
-// This made the code faster, read "Initializing instance variables" in
-// https://developers.google.com/speed/articles/optimizing-javascript
-u.prototype.nodes = [];
-
-// Add class(es) to the matched nodes
-u.prototype.addClass = function () {
- return this.eacharg(arguments, function (el, name) {
- el.classList.add(name);
- });
-};
-
-
-// [INTERNAL USE ONLY]
-// Add text in the specified position. It is used by other functions
-u.prototype.adjacent = function (html, data, callback) {
- if (typeof data === 'number') {
- if (data === 0) {
- data = [];
- } else {
- data = new Array(data).join().split(',').map(Number.call, Number);
- }
- }
-
- // Loop through all the nodes. It cannot reuse the eacharg() since the data
- // we want to do it once even if there's no "data" and we accept a selector
- return this.each(function (node, j) {
- var fragment = document.createDocumentFragment();
-
- // Allow for data to be falsy and still loop once
- u(data || {}).map(function (el, i) {
- // Allow for callbacks that accept some data
- var part = (typeof html === 'function') ? html.call(this, el, i, node, j) : html;
-
- if (typeof part === 'string') {
- return this.generate(part);
- }
-
- return u(part);
- }).each(function (n) {
- this.isInPage(n)
- ? fragment.appendChild(u(n).clone().first())
- : fragment.appendChild(n);
- });
-
- callback.call(this, node, fragment);
- });
-};
-
-// Add some html as a sibling after each of the matched elements.
-u.prototype.after = function (html, data) {
- return this.adjacent(html, data, function (node, fragment) {
- node.parentNode.insertBefore(fragment, node.nextSibling);
- });
-};
-
-
-// Add some html as a child at the end of each of the matched elements.
-u.prototype.append = function (html, data) {
- return this.adjacent(html, data, function (node, fragment) {
- node.appendChild(fragment);
- });
-};
-
-
-// [INTERNAL USE ONLY]
-
-// Normalize the arguments to an array of strings
-// Allow for several class names like "a b, c" and several parameters
-u.prototype.args = function (args, node, i) {
- if (typeof args === 'function') {
- args = args(node, i);
- }
-
- // First flatten it all to a string http://stackoverflow.com/q/22920305
- // If we try to slice a string bad things happen: ['n', 'a', 'm', 'e']
- if (typeof args !== 'string') {
- args = this.slice(args).map(this.str(node, i));
- }
-
- // Then convert that string to an array of not-null strings
- return args.toString().split(/[\s,]+/).filter(function (e) {
- return e.length;
- });
-};
-
-
-// Merge all of the nodes that the callback return into a simple array
-u.prototype.array = function (callback) {
- callback = callback;
- var self = this;
- return this.nodes.reduce(function (list, node, i) {
- var val;
- if (callback) {
- val = callback.call(self, node, i);
- if (!val) val = false;
- if (typeof val === 'string') val = u(val);
- if (val instanceof u) val = val.nodes;
- } else {
- val = node.innerHTML;
- }
- return list.concat(val !== false ? val : []);
- }, []);
-};
-
-
-// [INTERNAL USE ONLY]
-
-// Handle attributes for the matched elements
-u.prototype.attr = function (name, value, data) {
- data = data ? 'data-' : '';
-
- // This will handle those elements that can accept a pair with these footprints:
- // .attr('a'), .attr('a', 'b'), .attr({ a: 'b' })
- return this.pairs(name, value, function (node, name) {
- return node.getAttribute(data + name);
- }, function (node, name, value) {
- if (value) {
- node.setAttribute(data + name, value);
- } else {
- node.removeAttribute(data + name);
- }
- });
-};
-
-
-// Add some html before each of the matched elements.
-u.prototype.before = function (html, data) {
- return this.adjacent(html, data, function (node, fragment) {
- node.parentNode.insertBefore(fragment, node);
- });
-};
-
-
-// Get the direct children of all of the nodes with an optional filter
-u.prototype.children = function (selector) {
- return this.map(function (node) {
- return this.slice(node.children);
- }).filter(selector);
-};
-
-
-/**
- * Deep clone a DOM node and its descendants.
- * @return {[Object]} Returns an Umbrella.js instance.
- */
-u.prototype.clone = function () {
- return this.map(function (node, i) {
- var clone = node.cloneNode(true);
- var dest = this.getAll(clone);
-
- this.getAll(node).each(function (src, i) {
- for (var key in this.mirror) {
- if (this.mirror[key]) {
- this.mirror[key](src, dest.nodes[i]);
- }
- }
- });
-
- return clone;
- });
-};
-
-/**
- * Return an array of DOM nodes of a source node and its children.
- * @param {[Object]} context DOM node.
- * @param {[String]} tag DOM node tagName.
- * @return {[Array]} Array containing queried DOM nodes.
- */
-u.prototype.getAll = function getAll (context) {
- return u([context].concat(u('*', context).nodes));
-};
-
-// Store all of the operations to perform when cloning elements
-u.prototype.mirror = {};
-
-/**
- * Copy all JavaScript events of source node to destination node.
- * @param {[Object]} source DOM node
- * @param {[Object]} destination DOM node
- * @return {[undefined]]}
- */
-u.prototype.mirror.events = function (src, dest) {
- if (!src._e) return;
-
- for (var type in src._e) {
- src._e[type].forEach(function (ref) {
- u(dest).on(type, ref.callback);
- });
- }
-};
-
-/**
- * Copy select input value to its clone.
- * @param {[Object]} src DOM node
- * @param {[Object]} dest DOM node
- * @return {[undefined]}
- */
-u.prototype.mirror.select = function (src, dest) {
- if (u(src).is('select')) {
- dest.value = src.value;
- }
-};
-
-/**
- * Copy textarea input value to its clone
- * @param {[Object]} src DOM node
- * @param {[Object]} dest DOM node
- * @return {[undefined]}
- */
-u.prototype.mirror.textarea = function (src, dest) {
- if (u(src).is('textarea')) {
- dest.value = src.value;
- }
-};
-
-
-// Find the first ancestor that matches the selector for each node
-u.prototype.closest = function (selector) {
- return this.map(function (node) {
- // Keep going up and up on the tree. First element is also checked
- do {
- if (u(node).is(selector)) {
- return node;
- }
- } while ((node = node.parentNode) && node !== document);
- });
-};
-
-
-// Handle data-* attributes for the matched elements
-u.prototype.data = function (name, value) {
- return this.attr(name, value, true);
-};
-
-
-// Loops through every node from the current call
-u.prototype.each = function (callback) {
- // By doing callback.call we allow "this" to be the context for
- // the callback (see http://stackoverflow.com/q/4065353 precisely)
- this.nodes.forEach(callback.bind(this));
-
- return this;
-};
-
-
-// [INTERNAL USE ONLY]
-// Loop through the combination of every node and every argument passed
-u.prototype.eacharg = function (args, callback) {
- return this.each(function (node, i) {
- this.args(args, node, i).forEach(function (arg) {
- // Perform the callback for this node
- // By doing callback.call we allow "this" to be the context for
- // the callback (see http://stackoverflow.com/q/4065353 precisely)
- callback.call(this, node, arg);
- }, this);
- });
-};
-
-
-// Remove all children of the matched nodes from the DOM.
-u.prototype.empty = function () {
- return this.each(function (node) {
- while (node.firstChild) {
- node.removeChild(node.firstChild);
- }
- });
-};
-
-
-// .filter(selector)
-// Delete all of the nodes that don't pass the selector
-u.prototype.filter = function (selector) {
- // The default function if it's a CSS selector
- // Cannot change name to 'selector' since it'd mess with it inside this fn
- var callback = function (node) {
- // Make it compatible with some other browsers
- node.matches = node.matches || node.msMatchesSelector || node.webkitMatchesSelector;
-
- // Check if it's the same element (or any element if no selector was passed)
- return node.matches(selector || '*');
- };
-
- // filter() receives a function as in .filter(e => u(e).children().length)
- if (typeof selector === 'function') callback = selector;
-
- // filter() receives an instance of Umbrella as in .filter(u('a'))
- if (selector instanceof u) {
- callback = function (node) {
- return (selector.nodes).indexOf(node) !== -1;
- };
- }
-
- // Just a native filtering function for ultra-speed
- return u(this.nodes.filter(callback));
-};
-
-
-// Find all the nodes children of the current ones matched by a selector
-u.prototype.find = function (selector) {
- return this.map(function (node) {
- return u(selector || '*', node);
- });
-};
-
-
-// Get the first of the nodes
-u.prototype.first = function () {
- return this.nodes[0] || false;
-};
-
-
-// [INTERNAL USE ONLY]
-// Generate a fragment of HTML. This irons out the inconsistences
-u.prototype.generate = function (html) {
- // Table elements need to be child of for some f***ed up reason
- if (/^\s* ]/.test(html)) {
- return u(document.createElement('table')).html(html).children().children().nodes;
- } else if (/^\s* ]/.test(html)) {
- return u(document.createElement('table')).html(html).children().children().children().nodes;
- } else if (/^\s* 0;
-};
-
-
-/**
- * Internal use only. This function checks to see if an element is in the page's body. As contains is inclusive and determining if the body contains itself isn't the intention of isInPage this case explicitly returns false.
-https://developer.mozilla.org/en-US/docs/Web/API/Node/contains
- * @param {[Object]} node DOM node
- * @return {Boolean} The Node.contains() method returns a Boolean value indicating whether a node is a descendant of a given node or not.
- */
-u.prototype.isInPage = function isInPage (node) {
- return (node === document.body) ? false : document.body.contains(node);
-};
-
- // Get the last of the nodes
-u.prototype.last = function () {
- return this.nodes[this.length - 1] || false;
-};
-
-
-// Merge all of the nodes that the callback returns
-u.prototype.map = function (callback) {
- return callback ? u(this.array(callback)).unique() : this;
-};
-
-
-// Delete all of the nodes that equals the filter
-u.prototype.not = function (filter) {
- return this.filter(function (node) {
- return !u(node).is(filter || true);
- });
-};
-
-
-// Removes the callback to the event listener for each node
-u.prototype.off = function (events, cb, cb2) {
- var cb_filter_off = (cb == null && cb2 == null);
- var sel = null;
- var cb_to_be_removed = cb;
- if (typeof cb === 'string') {
- sel = cb;
- cb_to_be_removed = cb2;
- }
-
- return this.eacharg(events, function (node, event) {
- u(node._e ? node._e[event] : []).each(function (ref) {
- if (cb_filter_off || (ref.orig_callback === cb_to_be_removed && ref.selector === sel)) {
- node.removeEventListener(event, ref.callback);
- }
- });
- });
-};
-
-
-// Attach a callback to the specified events
-u.prototype.on = function (events, cb, cb2) {
- function overWriteCurrent (e, value) {
- try {
- Object.defineProperty(e, 'currentTarget', {
- value: value,
- configurable: true
- });
- } catch (err) {}
- }
-
- var selector = null;
- var orig_callback = cb;
- if (typeof cb === 'string') {
- selector = cb;
- orig_callback = cb2;
- cb = function (e) {
- var args = arguments;
- u(e.currentTarget)
- .find(selector)
- .each(function (target) {
- // The event is triggered either in the correct node, or a child
- // of the node that we are interested in
- // Note: .contains() will also check itself (besides children)
- if (!target.contains(e.target)) return;
-
- // If e.g. a child of a link was clicked, but we are listening
- // to the link, this will make the currentTarget the link itself,
- // so it's the "delegated" element instead of the root target. It
- // makes u('.render a').on('click') and u('.render').on('click', 'a')
- // to have the same currentTarget (the 'a')
- var curr = e.currentTarget;
- overWriteCurrent(e, target);
- cb2.apply(target, args);
- // Need to undo it afterwards, in case this event is reused in another
- // callback since otherwise u(e.currentTarget) above would break
- overWriteCurrent(e, curr);
- });
- };
- }
-
- var callback = function (e) {
- return cb.apply(this, [e].concat(e.detail || []));
- };
-
- return this.eacharg(events, function (node, event) {
- node.addEventListener(event, callback);
-
- // Store it so we can dereference it with `.off()` later on
- node._e = node._e || {};
- node._e[event] = node._e[event] || [];
- node._e[event].push({
- callback: callback,
- orig_callback: orig_callback,
- selector: selector
- });
- });
-};
-
-
-// [INTERNAL USE ONLY]
-
-// Take the arguments and a couple of callback to handle the getter/setter pairs
-// such as: .css('a'), .css('a', 'b'), .css({ a: 'b' })
-u.prototype.pairs = function (name, value, get, set) {
- // Convert it into a plain object if it is not
- if (typeof value !== 'undefined') {
- var nm = name;
- name = {};
- name[nm] = value;
- }
-
- if (typeof name === 'object') {
- // Set the value of each one, for each of the { prop: value } pairs
- return this.each(function (node, i) {
- for (var key in name) {
- if (typeof name[key] === 'function') {
- set(node, key, name[key](node, i));
- } else {
- set(node, key, name[key]);
- }
- }
- });
- }
-
- // Return the style of the first one
- return this.length ? get(this.first(), name) : '';
-};
-
-// [INTERNAL USE ONLY]
-
-// Parametize an object: { a: 'b', c: 'd' } => 'a=b&c=d'
-u.prototype.param = function (obj) {
- return Object.keys(obj).map(function (key) {
- return this.uri(key) + '=' + this.uri(obj[key]);
- }.bind(this)).join('&');
-};
-
-// Travel the matched elements one node up
-u.prototype.parent = function (selector) {
- return this.map(function (node) {
- return node.parentNode;
- }).filter(selector);
-};
-
-
-// Add nodes at the beginning of each node
-u.prototype.prepend = function (html, data) {
- return this.adjacent(html, data, function (node, fragment) {
- node.insertBefore(fragment, node.firstChild);
- });
-};
-
-
-// Delete the matched nodes from the DOM
-u.prototype.remove = function () {
- // Loop through all the nodes
- return this.each(function (node) {
- // Perform the removal only if the node has a parent
- if (node.parentNode) {
- node.parentNode.removeChild(node);
- }
- });
-};
-
-
-// Removes a class from all of the matched nodes
-u.prototype.removeClass = function () {
- // Loop the combination of each node with each argument
- return this.eacharg(arguments, function (el, name) {
- // Remove the class using the native method
- el.classList.remove(name);
- });
-};
-
-
-// Replace the matched elements with the passed argument.
-u.prototype.replace = function (html, data) {
- var nodes = [];
- this.adjacent(html, data, function (node, fragment) {
- nodes = nodes.concat(this.slice(fragment.children));
- node.parentNode.replaceChild(fragment, node);
- });
- return u(nodes);
-};
-
-
-// Scroll to the first matched element
-u.prototype.scroll = function () {
- this.first().scrollIntoView({ behavior: 'smooth' });
- return this;
-};
-
-
-// [INTERNAL USE ONLY]
-// Select the adequate part from the context
-u.prototype.select = function (parameter, context) {
- // Allow for spaces before or after
- parameter = parameter.replace(/^\s*/, '').replace(/\s*$/, '');
-
- if (/^'),
- // 2) clone the currently matched node
- // 3) append cloned dom node to constructed node based on selector
- return this.map(function (node) {
- return u(selector).each(function (n) {
- findDeepestNode(n)
- .append(node.cloneNode(true));
-
- node
- .parentNode
- .replaceChild(n, node);
- });
- });
-};
-
-// Export it for webpack
-if (typeof module === 'object' && module.exports) {
- // Avoid breaking it for `import { u } from ...`. Add `import u from ...`
- module.exports = u;
- module.exports.u = u;
-}
-
diff --git a/gno.land/pkg/gnoweb/static/js/umbrella.min.js b/gno.land/pkg/gnoweb/static/js/umbrella.min.js
deleted file mode 100644
index 70ecb99cc32..00000000000
--- a/gno.land/pkg/gnoweb/static/js/umbrella.min.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/* Umbrella JS 3.3.0 umbrellajs.com */
-
-var u=function(t,e){return this instanceof u?t instanceof u?t:((t="string"==typeof t?this.select(t,e):t)&&t.nodeName&&(t=[t]),void(this.nodes=this.slice(t))):new u(t,e)};u.prototype={get length(){return this.nodes.length}},u.prototype.nodes=[],u.prototype.addClass=function(){return this.eacharg(arguments,function(t,e){t.classList.add(e)})},u.prototype.adjacent=function(o,t,i){return"number"==typeof t&&(t=0===t?[]:new Array(t).join().split(",").map(Number.call,Number)),this.each(function(n,r){var e=document.createDocumentFragment();u(t||{}).map(function(t,e){e="function"==typeof o?o.call(this,t,e,n,r):o;return"string"==typeof e?this.generate(e):u(e)}).each(function(t){this.isInPage(t)?e.appendChild(u(t).clone().first()):e.appendChild(t)}),i.call(this,n,e)})},u.prototype.after=function(t,e){return this.adjacent(t,e,function(t,e){t.parentNode.insertBefore(e,t.nextSibling)})},u.prototype.append=function(t,e){return this.adjacent(t,e,function(t,e){t.appendChild(e)})},u.prototype.args=function(t,e,n){return(t="string"!=typeof(t="function"==typeof t?t(e,n):t)?this.slice(t).map(this.str(e,n)):t).toString().split(/[\s,]+/).filter(function(t){return t.length})},u.prototype.array=function(o){var i=this;return this.nodes.reduce(function(t,e,n){var r;return o?(r="string"==typeof(r=(r=o.call(i,e,n))||!1)?u(r):r)instanceof u&&(r=r.nodes):r=e.innerHTML,t.concat(!1!==r?r:[])},[])},u.prototype.attr=function(t,e,r){return r=r?"data-":"",this.pairs(t,e,function(t,e){return t.getAttribute(r+e)},function(t,e,n){n?t.setAttribute(r+e,n):t.removeAttribute(r+e)})},u.prototype.before=function(t,e){return this.adjacent(t,e,function(t,e){t.parentNode.insertBefore(e,t)})},u.prototype.children=function(t){return this.map(function(t){return this.slice(t.children)}).filter(t)},u.prototype.clone=function(){return this.map(function(t,e){var n=t.cloneNode(!0),r=this.getAll(n);return this.getAll(t).each(function(t,e){for(var n in this.mirror)this.mirror[n]&&this.mirror[n](t,r.nodes[e])}),n})},u.prototype.getAll=function(t){return u([t].concat(u("*",t).nodes))},u.prototype.mirror={},u.prototype.mirror.events=function(t,e){if(t._e)for(var n in t._e)t._e[n].forEach(function(t){u(e).on(n,t.callback)})},u.prototype.mirror.select=function(t,e){u(t).is("select")&&(e.value=t.value)},u.prototype.mirror.textarea=function(t,e){u(t).is("textarea")&&(e.value=t.value)},u.prototype.closest=function(e){return this.map(function(t){do{if(u(t).is(e))return t}while((t=t.parentNode)&&t!==document)})},u.prototype.data=function(t,e){return this.attr(t,e,!0)},u.prototype.each=function(t){return this.nodes.forEach(t.bind(this)),this},u.prototype.eacharg=function(n,r){return this.each(function(e,t){this.args(n,e,t).forEach(function(t){r.call(this,e,t)},this)})},u.prototype.empty=function(){return this.each(function(t){for(;t.firstChild;)t.removeChild(t.firstChild)})},u.prototype.filter=function(e){var t=e instanceof u?function(t){return-1!==e.nodes.indexOf(t)}:"function"==typeof e?e:function(t){return t.matches=t.matches||t.msMatchesSelector||t.webkitMatchesSelector,t.matches(e||"*")};return u(this.nodes.filter(t))},u.prototype.find=function(e){return this.map(function(t){return u(e||"*",t)})},u.prototype.first=function(){return this.nodes[0]||!1},u.prototype.generate=function(t){return/^\s* ]/.test(t)?u(document.createElement("table")).html(t).children().children().nodes:/^\s* ]/.test(t)?u(document.createElement("table")).html(t).children().children().children().nodes:/^\s*= 360 || s < 0 || s > 1 || l < 0 || l > 1 {
+ return 0, 0, 0
+ }
+
+ C := (1 - math.Abs((2*l)-1)) * s
+ X := C * (1 - math.Abs(math.Mod(h/60, 2)-1))
+ m := l - (C / 2)
+
+ var rNot, gNot, bNot float64
+ switch {
+ case 0 <= h && h < 60:
+ rNot, gNot, bNot = C, X, 0
+ case 60 <= h && h < 120:
+ rNot, gNot, bNot = X, C, 0
+ case 120 <= h && h < 180:
+ rNot, gNot, bNot = 0, C, X
+ case 180 <= h && h < 240:
+ rNot, gNot, bNot = 0, X, C
+ case 240 <= h && h < 300:
+ rNot, gNot, bNot = X, 0, C
+ case 300 <= h && h < 360:
+ rNot, gNot, bNot = C, 0, X
+ }
+
+ r = uint8(math.Round((rNot + m) * 255))
+ g = uint8(math.Round((gNot + m) * 255))
+ b = uint8(math.Round((bNot + m) * 255))
+ return r, g, b
+}
+
+func rgbToHex(r, g, b uint8) string {
+ return fmt.Sprintf("#%02X%02X%02X", r, g, b)
+}
diff --git a/gno.land/pkg/gnoweb/tools/cmd/logname/main.go b/gno.land/pkg/gnoweb/tools/cmd/logname/main.go
new file mode 100644
index 00000000000..50b42b79c0a
--- /dev/null
+++ b/gno.land/pkg/gnoweb/tools/cmd/logname/main.go
@@ -0,0 +1,41 @@
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+
+ "github.com/charmbracelet/lipgloss"
+)
+
+func main() {
+ if len(os.Args) < 1 {
+ fmt.Fprintln(os.Stderr, "invalid name")
+ os.Exit(1)
+ }
+
+ name := os.Args[1]
+
+ const width = 12
+ if len(name) >= width {
+ name = name[:width-3] + "..."
+ }
+
+ colorLeft := colorFromString(name, 0.5, 0.6, 90)
+ colorRight := colorFromString(name, 1.0, 0.92, 90)
+ borderStyle := lipgloss.NewStyle().Foreground(colorLeft).
+ Border(lipgloss.ThickBorder(), false, true, false, false).
+ BorderForeground(colorLeft).
+ Bold(true).
+ Width(width)
+ lineStyle := lipgloss.NewStyle().Foreground(colorRight)
+
+ w, r := os.Stdout, os.Stdin
+
+ scanner := bufio.NewScanner(r)
+ for scanner.Scan() {
+ line := scanner.Text()
+ fmt.Fprint(w, borderStyle.Render(name)+" ")
+ fmt.Fprintln(w, lineStyle.Render(line))
+ }
+}
diff --git a/gno.land/pkg/gnoweb/tools/go.mod b/gno.land/pkg/gnoweb/tools/go.mod
new file mode 100644
index 00000000000..1b50c9d52dd
--- /dev/null
+++ b/gno.land/pkg/gnoweb/tools/go.mod
@@ -0,0 +1,25 @@
+module tools
+
+go 1.22.3
+
+require (
+ github.com/cespare/reflex v0.3.1
+ github.com/charmbracelet/lipgloss v0.11.0
+)
+
+require (
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/charmbracelet/x/ansi v0.1.1 // indirect
+ github.com/creack/pty v1.1.21 // indirect
+ github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
+ github.com/kr/pretty v0.3.1 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-runewidth v0.0.15 // indirect
+ github.com/muesli/termenv v0.15.2 // indirect
+ github.com/ogier/pflag v0.0.1 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/rogpeppe/go-internal v1.12.0 // indirect
+ golang.org/x/sys v0.21.0 // indirect
+)
diff --git a/gno.land/pkg/gnoweb/tools/go.sum b/gno.land/pkg/gnoweb/tools/go.sum
new file mode 100644
index 00000000000..7eec65cb8ec
--- /dev/null
+++ b/gno.land/pkg/gnoweb/tools/go.sum
@@ -0,0 +1,45 @@
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/cespare/reflex v0.3.1 h1:N4Y/UmRrjwOkNT0oQQnYsdr6YBxvHqtSfPB4mqOyAKk=
+github.com/cespare/reflex v0.3.1/go.mod h1:I+0Pnu2W693i7Hv6ZZG76qHTY0mgUa7uCIfCtikXojE=
+github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g=
+github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8=
+github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk=
+github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
+github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
+github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
+github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750=
+github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
+golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
diff --git a/gno.land/pkg/gnoweb/tools/tools.go b/gno.land/pkg/gnoweb/tools/tools.go
new file mode 100644
index 00000000000..a444aecbcea
--- /dev/null
+++ b/gno.land/pkg/gnoweb/tools/tools.go
@@ -0,0 +1,5 @@
+package tools
+
+import (
+ _ "github.com/cespare/reflex"
+)
diff --git a/gno.land/pkg/gnoweb/url.go b/gno.land/pkg/gnoweb/url.go
new file mode 100644
index 00000000000..9127225d490
--- /dev/null
+++ b/gno.land/pkg/gnoweb/url.go
@@ -0,0 +1,256 @@
+package gnoweb
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+ "path/filepath"
+ "regexp"
+ "slices"
+ "strings"
+)
+
+var ErrURLInvalidPath = errors.New("invalid path")
+
+// rePkgOrRealmPath matches and validates a flexible path.
+var rePkgOrRealmPath = regexp.MustCompile(`^/[a-z][a-z0-9_/]*$`)
+
+// GnoURL decomposes the parts of an URL to query a realm.
+type GnoURL struct {
+ // Example full path:
+ // gno.land/r/demo/users/render.gno:jae$help&a=b?c=d
+
+ Domain string // gno.land
+ Path string // /r/demo/users
+ Args string // jae
+ WebQuery url.Values // help&a=b
+ Query url.Values // c=d
+ File string // render.gno
+}
+
+// EncodeFlag is used to specify which URL components to encode.
+type EncodeFlag int
+
+const (
+ EncodeDomain EncodeFlag = 1 << iota // Encode the domain component
+ EncodePath // Encode the path component
+ EncodeArgs // Encode the arguments component
+ EncodeWebQuery // Encode the web query component
+ EncodeQuery // Encode the query component
+ EncodeNoEscape // Disable escaping of arguments
+)
+
+// Encode constructs a URL string from the components of a GnoURL struct,
+// encoding the specified components based on the provided EncodeFlag bitmask.
+//
+// The function selectively encodes the URL's path, arguments, web query, and
+// query parameters, depending on the flags set in encodeFlags.
+//
+// Returns a string representing the encoded URL.
+//
+// Example:
+//
+// gnoURL := GnoURL{
+// Domain: "gno.land",
+// Path: "/r/demo/users",
+// Args: "john",
+// File: "render.gno",
+// }
+//
+// encodedURL := gnoURL.Encode(EncodePath | EncodeArgs)
+// fmt.Println(encodedURL) // Output: /r/demo/users/render.gno:john
+//
+// URL components are encoded using url.PathEscape unless EncodeNoEscape is specified.
+func (gnoURL GnoURL) Encode(encodeFlags EncodeFlag) string {
+ var urlstr strings.Builder
+
+ noEscape := encodeFlags.Has(EncodeNoEscape)
+
+ if encodeFlags.Has(EncodeDomain) {
+ urlstr.WriteString(gnoURL.Domain)
+ }
+
+ if encodeFlags.Has(EncodePath) {
+ path := gnoURL.Path
+ urlstr.WriteString(path)
+ }
+
+ if len(gnoURL.File) > 0 {
+ urlstr.WriteRune('/')
+ urlstr.WriteString(gnoURL.File)
+ }
+
+ if encodeFlags.Has(EncodeArgs) && gnoURL.Args != "" {
+ if encodeFlags.Has(EncodePath) {
+ urlstr.WriteRune(':')
+ }
+
+ // XXX: Arguments should ideally always be escaped,
+ // but this may require changes in some realms.
+ args := gnoURL.Args
+ if !noEscape {
+ args = escapeDollarSign(url.PathEscape(args))
+ }
+
+ urlstr.WriteString(args)
+ }
+
+ if encodeFlags.Has(EncodeWebQuery) && len(gnoURL.WebQuery) > 0 {
+ urlstr.WriteRune('$')
+ if noEscape {
+ urlstr.WriteString(NoEscapeQuery(gnoURL.WebQuery))
+ } else {
+ urlstr.WriteString(gnoURL.WebQuery.Encode())
+ }
+ }
+
+ if encodeFlags.Has(EncodeQuery) && len(gnoURL.Query) > 0 {
+ urlstr.WriteRune('?')
+ if noEscape {
+ urlstr.WriteString(NoEscapeQuery(gnoURL.Query))
+ } else {
+ urlstr.WriteString(gnoURL.Query.Encode())
+ }
+ }
+
+ return urlstr.String()
+}
+
+// Has checks if the EncodeFlag contains all the specified flags.
+func (f EncodeFlag) Has(flags EncodeFlag) bool {
+ return f&flags != 0
+}
+
+func escapeDollarSign(s string) string {
+ return strings.ReplaceAll(s, "$", "%24")
+}
+
+// EncodeArgs encodes the arguments and query parameters into a string.
+// This function is intended to be passed as a realm `Render` argument.
+func (gnoURL GnoURL) EncodeArgs() string {
+ return gnoURL.Encode(EncodeArgs | EncodeQuery | EncodeNoEscape)
+}
+
+// EncodeURL encodes the path, arguments, and query parameters into a string.
+// This function provides the full representation of the URL without the web query.
+func (gnoURL GnoURL) EncodeURL() string {
+ return gnoURL.Encode(EncodePath | EncodeArgs | EncodeQuery)
+}
+
+// EncodeWebURL encodes the path, package arguments, web query, and query into a string.
+// This function provides the full representation of the URL.
+func (gnoURL GnoURL) EncodeWebURL() string {
+ return gnoURL.Encode(EncodePath | EncodeArgs | EncodeWebQuery | EncodeQuery)
+}
+
+// IsPure checks if the URL path represents a pure path.
+func (gnoURL GnoURL) IsPure() bool {
+ return strings.HasPrefix(gnoURL.Path, "/p/")
+}
+
+// IsRealm checks if the URL path represents a realm path.
+func (gnoURL GnoURL) IsRealm() bool {
+ return strings.HasPrefix(gnoURL.Path, "/r/")
+}
+
+// IsFile checks if the URL path represents a file.
+func (gnoURL GnoURL) IsFile() bool {
+ return gnoURL.File != ""
+}
+
+// IsDir checks if the URL path represents a directory.
+func (gnoURL GnoURL) IsDir() bool {
+ return !gnoURL.IsFile() &&
+ len(gnoURL.Path) > 0 && gnoURL.Path[len(gnoURL.Path)-1] == '/'
+}
+
+func (gnoURL GnoURL) IsValid() bool {
+ return rePkgOrRealmPath.MatchString(gnoURL.Path)
+}
+
+// ParseGnoURL parses a URL into a GnoURL structure, extracting and validating its components.
+func ParseGnoURL(u *url.URL) (*GnoURL, error) {
+ var webargs string
+ path, args, found := strings.Cut(u.EscapedPath(), ":")
+ if found {
+ args, webargs, _ = strings.Cut(args, "$")
+ } else {
+ path, webargs, _ = strings.Cut(path, "$")
+ }
+
+ upath, err := url.PathUnescape(path)
+ if err != nil {
+ return nil, fmt.Errorf("unable to unescape path %q: %w", path, err)
+ }
+
+ var file string
+
+ // A file is considered as one that either ends with an extension or
+ // contains an uppercase rune
+ ext := filepath.Ext(upath)
+ base := filepath.Base(upath)
+ if ext != "" || strings.ToLower(base) != base {
+ file = base
+ upath = strings.TrimSuffix(upath, base)
+
+ // Trim last slash if any
+ if i := strings.LastIndexByte(upath, '/'); i > 0 {
+ upath = upath[:i]
+ }
+ }
+
+ if !rePkgOrRealmPath.MatchString(upath) {
+ return nil, fmt.Errorf("%w: %q", ErrURLInvalidPath, upath)
+ }
+
+ webquery := url.Values{}
+ if len(webargs) > 0 {
+ var parseErr error
+ if webquery, parseErr = url.ParseQuery(webargs); parseErr != nil {
+ return nil, fmt.Errorf("unable to parse webquery %q: %w", webargs, parseErr)
+ }
+ }
+
+ uargs, err := url.PathUnescape(args)
+ if err != nil {
+ return nil, fmt.Errorf("unable to unescape args %q: %w", args, err)
+ }
+
+ return &GnoURL{
+ Path: upath,
+ Args: uargs,
+ WebQuery: webquery,
+ Query: u.Query(),
+ Domain: u.Hostname(),
+ File: file,
+ }, nil
+}
+
+// NoEscapeQuery generates a URL-encoded query string from the given url.Values,
+// without escaping the keys and values. The query parameters are sorted by key.
+func NoEscapeQuery(v url.Values) string {
+ // Encode encodes the values into “URL encoded” form
+ // ("bar=baz&foo=quux") sorted by key.
+ if len(v) == 0 {
+ return ""
+ }
+ var buf strings.Builder
+ keys := make([]string, 0, len(v))
+ for k := range v {
+ keys = append(keys, k)
+ }
+ slices.Sort(keys)
+ for _, k := range keys {
+ vs := v[k]
+ keyEscaped := k
+ for _, v := range vs {
+ if buf.Len() > 0 {
+ buf.WriteByte('&')
+ }
+ buf.WriteString(keyEscaped)
+ buf.WriteByte('=')
+ buf.WriteString(v)
+ }
+ }
+ return buf.String()
+}
diff --git a/gno.land/pkg/gnoweb/url_test.go b/gno.land/pkg/gnoweb/url_test.go
new file mode 100644
index 00000000000..7a491eaa149
--- /dev/null
+++ b/gno.land/pkg/gnoweb/url_test.go
@@ -0,0 +1,462 @@
+package gnoweb
+
+import (
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseGnoURL(t *testing.T) {
+ testCases := []struct {
+ Name string
+ Input string
+ Expected *GnoURL
+ Err error
+ }{
+ {
+ Name: "malformed url",
+ Input: "https://gno.land/r/dem)o:$?",
+ Expected: nil,
+ Err: ErrURLInvalidPath,
+ },
+
+ {
+ Name: "simple",
+ Input: "https://gno.land/r/simple/test",
+ Expected: &GnoURL{
+ Domain: "gno.land",
+ Path: "/r/simple/test",
+ WebQuery: url.Values{},
+ Query: url.Values{},
+ },
+ },
+
+ {
+ Name: "file",
+ Input: "https://gno.land/r/simple/test/encode.gno",
+ Expected: &GnoURL{
+ Domain: "gno.land",
+ Path: "/r/simple/test",
+ WebQuery: url.Values{},
+ Query: url.Values{},
+ File: "encode.gno",
+ },
+ },
+
+ {
+ Name: "complex file path",
+ Input: "https://gno.land/r/simple/test///...gno",
+ Expected: &GnoURL{
+ Domain: "gno.land",
+ Path: "/r/simple/test//",
+ WebQuery: url.Values{},
+ Query: url.Values{},
+ File: "...gno",
+ },
+ },
+
+ {
+ Name: "webquery + query",
+ Input: "https://gno.land/r/demo/foo$help&func=Bar&name=Baz",
+ Expected: &GnoURL{
+ Path: "/r/demo/foo",
+ Args: "",
+ WebQuery: url.Values{
+ "help": []string{""},
+ "func": []string{"Bar"},
+ "name": []string{"Baz"},
+ },
+ Query: url.Values{},
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "path args + webquery",
+ Input: "https://gno.land/r/demo/foo:example$tz=Europe/Paris",
+ Expected: &GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example",
+ WebQuery: url.Values{
+ "tz": []string{"Europe/Paris"},
+ },
+ Query: url.Values{},
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "path args + webquery + query",
+ Input: "https://gno.land/r/demo/foo:example$tz=Europe/Paris?hello=42",
+ Expected: &GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example",
+ WebQuery: url.Values{
+ "tz": []string{"Europe/Paris"},
+ },
+ Query: url.Values{
+ "hello": []string{"42"},
+ },
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "webquery inside query",
+ Input: "https://gno.land/r/demo/foo:example?value=42$tz=Europe/Paris",
+ Expected: &GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example",
+ WebQuery: url.Values{},
+ Query: url.Values{
+ "value": []string{"42$tz=Europe/Paris"},
+ },
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "webquery escaped $",
+ Input: "https://gno.land/r/demo/foo:example%24hello=43$hello=42",
+ Expected: &GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example$hello=43",
+ WebQuery: url.Values{
+ "hello": []string{"42"},
+ },
+ Query: url.Values{},
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "unknown path kind",
+ Input: "https://gno.land/x/demo/foo",
+ Expected: &GnoURL{
+ Path: "/x/demo/foo",
+ Args: "",
+ WebQuery: url.Values{},
+ Query: url.Values{},
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "empty path",
+ Input: "https://gno.land/r/",
+ Expected: &GnoURL{
+ Path: "/r/",
+ Args: "",
+ WebQuery: url.Values{},
+ Query: url.Values{},
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "complex query",
+ Input: "https://gno.land/r/demo/foo$help?func=Bar&name=Baz&age=30",
+ Expected: &GnoURL{
+ Path: "/r/demo/foo",
+ Args: "",
+ WebQuery: url.Values{
+ "help": []string{""},
+ },
+ Query: url.Values{
+ "func": []string{"Bar"},
+ "name": []string{"Baz"},
+ "age": []string{"30"},
+ },
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "multiple web queries",
+ Input: "https://gno.land/r/demo/foo$help&func=Bar$test=123",
+ Expected: &GnoURL{
+ Path: "/r/demo/foo",
+ Args: "",
+ WebQuery: url.Values{
+ "help": []string{""},
+ "func": []string{"Bar$test=123"},
+ },
+ Query: url.Values{},
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "webquery-args-webquery",
+ Input: "https://gno.land/r/demo/aaa$bbb:CCC&DDD$EEE",
+ Err: ErrURLInvalidPath, // `/r/demo/aaa$bbb` is an invalid path
+ },
+
+ {
+ Name: "args-webquery-args",
+ Input: "https://gno.land/r/demo/aaa:BBB$CCC&DDD:EEE",
+ Expected: &GnoURL{
+ Domain: "gno.land",
+ Path: "/r/demo/aaa",
+ Args: "BBB",
+ WebQuery: url.Values{
+ "CCC": []string{""},
+ "DDD:EEE": []string{""},
+ },
+ Query: url.Values{},
+ },
+ },
+
+ {
+ Name: "escaped characters in args",
+ Input: "https://gno.land/r/demo/foo:example%20with%20spaces$tz=Europe/Paris",
+ Expected: &GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example with spaces",
+ WebQuery: url.Values{
+ "tz": []string{"Europe/Paris"},
+ },
+ Query: url.Values{},
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "file in path + args + query",
+ Input: "https://gno.land/r/demo/foo/render.gno:example$tz=Europe/Paris",
+ Expected: &GnoURL{
+ Path: "/r/demo/foo",
+ File: "render.gno",
+ Args: "example",
+ WebQuery: url.Values{
+ "tz": []string{"Europe/Paris"},
+ },
+ Query: url.Values{},
+ Domain: "gno.land",
+ },
+ },
+
+ {
+ Name: "no extension file",
+ Input: "https://gno.land/r/demo/lIcEnSe",
+ Expected: &GnoURL{
+ Path: "/r/demo",
+ File: "lIcEnSe",
+ Args: "",
+ WebQuery: url.Values{},
+ Query: url.Values{},
+ Domain: "gno.land",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.Name, func(t *testing.T) {
+ t.Logf("testing input: %q", tc.Input)
+
+ u, err := url.Parse(tc.Input)
+ require.NoError(t, err)
+
+ result, err := ParseGnoURL(u)
+ if tc.Err == nil {
+ require.NoError(t, err)
+ t.Logf("encoded web path: %q", result.EncodeWebURL())
+ } else {
+ require.Error(t, err)
+ require.ErrorIs(t, err, tc.Err)
+ }
+
+ assert.Equal(t, tc.Expected, result)
+ })
+ }
+}
+
+func TestEncode(t *testing.T) {
+ testCases := []struct {
+ Name string
+ GnoURL GnoURL
+ EncodeFlags EncodeFlag
+ Expected string
+ }{
+ {
+ Name: "encode domain",
+ GnoURL: GnoURL{
+ Domain: "gno.land",
+ Path: "/r/demo/foo",
+ },
+ EncodeFlags: EncodeDomain,
+ Expected: "gno.land",
+ },
+
+ {
+ Name: "encode web query without escape",
+ GnoURL: GnoURL{
+ Domain: "gno.land",
+ Path: "/r/demo/foo",
+ WebQuery: url.Values{
+ "help": []string{""},
+ "fun$c": []string{"B$ ar"},
+ },
+ },
+ EncodeFlags: EncodeWebQuery | EncodeNoEscape,
+ Expected: "$fun$c=B$ ar&help=",
+ },
+
+ {
+ Name: "encode domain and path",
+ GnoURL: GnoURL{
+ Domain: "gno.land",
+ Path: "/r/demo/foo",
+ },
+ EncodeFlags: EncodeDomain | EncodePath,
+ Expected: "gno.land/r/demo/foo",
+ },
+
+ {
+ Name: "Encode Path Only",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ },
+ EncodeFlags: EncodePath,
+ Expected: "/r/demo/foo",
+ },
+
+ {
+ Name: "Encode Path and File",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ File: "render.gno",
+ },
+ EncodeFlags: EncodePath,
+ Expected: "/r/demo/foo/render.gno",
+ },
+
+ {
+ Name: "Encode Path, File, and Args",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ File: "render.gno",
+ Args: "example",
+ },
+ EncodeFlags: EncodePath | EncodeArgs,
+ Expected: "/r/demo/foo/render.gno:example",
+ },
+
+ {
+ Name: "Encode Path and Args",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example",
+ },
+ EncodeFlags: EncodePath | EncodeArgs,
+ Expected: "/r/demo/foo:example",
+ },
+
+ {
+ Name: "Encode Path, Args, and WebQuery",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example",
+ WebQuery: url.Values{
+ "tz": []string{"Europe/Paris"},
+ },
+ },
+ EncodeFlags: EncodePath | EncodeArgs | EncodeWebQuery,
+ Expected: "/r/demo/foo:example$tz=Europe%2FParis",
+ },
+
+ {
+ Name: "Encode Full URL",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example",
+ WebQuery: url.Values{
+ "tz": []string{"Europe/Paris"},
+ },
+ Query: url.Values{
+ "hello": []string{"42"},
+ },
+ },
+ EncodeFlags: EncodePath | EncodeArgs | EncodeWebQuery | EncodeQuery,
+ Expected: "/r/demo/foo:example$tz=Europe%2FParis?hello=42",
+ },
+
+ {
+ Name: "Encode Args and Query",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "hello Jo$ny",
+ Query: url.Values{
+ "hello": []string{"42"},
+ },
+ },
+ EncodeFlags: EncodeArgs | EncodeQuery,
+ Expected: "hello%20Jo%24ny?hello=42",
+ },
+
+ {
+ Name: "Encode Args and Query (No Escape)",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "hello Jo$ny",
+ Query: url.Values{
+ "hello": []string{"42"},
+ },
+ },
+ EncodeFlags: EncodeArgs | EncodeQuery | EncodeNoEscape,
+ Expected: "hello Jo$ny?hello=42",
+ },
+
+ {
+ Name: "Encode Args and Query",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example",
+ Query: url.Values{
+ "hello": []string{"42"},
+ },
+ },
+ EncodeFlags: EncodeArgs | EncodeQuery,
+ Expected: "example?hello=42",
+ },
+
+ {
+ Name: "Encode with Escaped Characters",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example with spaces",
+ WebQuery: url.Values{
+ "tz": []string{"Europe/Paris"},
+ },
+ Query: url.Values{
+ "hello": []string{"42"},
+ },
+ },
+ EncodeFlags: EncodePath | EncodeArgs | EncodeWebQuery | EncodeQuery,
+ Expected: "/r/demo/foo:example%20with%20spaces$tz=Europe%2FParis?hello=42",
+ },
+
+ {
+ Name: "Encode Path, Args, and Query",
+ GnoURL: GnoURL{
+ Path: "/r/demo/foo",
+ Args: "example",
+ Query: url.Values{
+ "hello": []string{"42"},
+ },
+ },
+ EncodeFlags: EncodePath | EncodeArgs | EncodeQuery,
+ Expected: "/r/demo/foo:example?hello=42",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.Name, func(t *testing.T) {
+ result := tc.GnoURL.Encode(tc.EncodeFlags)
+ require.True(t, tc.GnoURL.IsValid(), "gno url is not valid")
+ assert.Equal(t, tc.Expected, result)
+ })
+ }
+}
diff --git a/gno.land/pkg/gnoweb/views/404.html b/gno.land/pkg/gnoweb/views/404.html
deleted file mode 100644
index fee4fff8689..00000000000
--- a/gno.land/pkg/gnoweb/views/404.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{{- define "app" -}}
-
-
-
- {{ template "html_head" . }}
- 404 - Not Found
-
-
-
-
-
{{.Data.title}}
-
{{.Data.path}}
-
-
- {{ template "analytics" .}}
-
-
-{{- end -}}
diff --git a/gno.land/pkg/gnoweb/views/faucet.html b/gno.land/pkg/gnoweb/views/faucet.html
deleted file mode 100644
index 1c9ca1de53c..00000000000
--- a/gno.land/pkg/gnoweb/views/faucet.html
+++ /dev/null
@@ -1,97 +0,0 @@
-{{- define "app" -}}
-
-
-
- {{ template "html_head" . }}
- Gno.land
-
-
-
-
-
- This is the gno.land (test) {{ if .Data.Config.CaptchaSite }}
-
- {{ end }}
-
-
-
-
-
-
-
Faucet Response:
-
- {{ template "footer" }}
-
- {{ template "js" }}
-
-
-{{- end -}}
diff --git a/gno.land/pkg/gnoweb/views/funcs.html b/gno.land/pkg/gnoweb/views/funcs.html
deleted file mode 100644
index 37c63458515..00000000000
--- a/gno.land/pkg/gnoweb/views/funcs.html
+++ /dev/null
@@ -1,220 +0,0 @@
-{{- define "header_buttons" -}}
-
-{{- end -}}
-
-{{- define "html_head" -}}
-
-{{if .Data.Description}} {{end}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{{- end -}}
-
-{{- define "logo" -}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{{- end -}}
-
-{{- define "footer" -}}
-
-{{- end -}}
-
-{{- define "js" -}}
-
-
-
-
-
-
-{{ template "analytics" .}}
-{{- end -}}
-
-{{- define "analytics" -}}
-{{- if .Data.Config.WithAnalytics -}}
-
-
-
-{{- end -}}
-{{- end -}}
-
-{{- define "subscribe" -}}
-
-
-
-{{- end -}}
diff --git a/gno.land/pkg/gnoweb/views/generic.html b/gno.land/pkg/gnoweb/views/generic.html
deleted file mode 100644
index 5bcd14c3a46..00000000000
--- a/gno.land/pkg/gnoweb/views/generic.html
+++ /dev/null
@@ -1,21 +0,0 @@
-{{- define "app" -}}
-
-
-
- Gno.land - {{ .Data.Title }}
- {{ template "html_head" . }}
-
-
-
-
-
-
- {{- .Data.MainContent -}}
-
-
- {{ template "footer" }}
-
- {{ template "js" .}}
-
-
-{{- end -}}
diff --git a/gno.land/pkg/gnoweb/views/package_dir.html b/gno.land/pkg/gnoweb/views/package_dir.html
deleted file mode 100644
index 793ebd40b84..00000000000
--- a/gno.land/pkg/gnoweb/views/package_dir.html
+++ /dev/null
@@ -1,33 +0,0 @@
-{{- define "app" -}}
-
-
-
- {{ template "html_head" . }}
- Gno.land - {{.Data.DirPath}}
-
-
-
-
-
-
{{ template "dir_contents" . }}
- {{ template "footer" }}
-
- {{ template "js" . }}
-
-
-{{- end -}}
-
-{{- define "dir_contents" -}}
-
- {{ $dirPath := .Data.DirPath }}
-
- {{ range .Data.Files }}
-
- {{ . }}
-
- {{ end }}
-
-
-{{- end -}}
diff --git a/gno.land/pkg/gnoweb/views/package_file.html b/gno.land/pkg/gnoweb/views/package_file.html
deleted file mode 100644
index 43e7820b29f..00000000000
--- a/gno.land/pkg/gnoweb/views/package_file.html
+++ /dev/null
@@ -1,23 +0,0 @@
-{{- define "app" -}}
-
-
-
- {{ template "html_head" . }}
- Gno.land - {{.Data.DirPath}}/{{.Data.FileName}}
-
-
-
-
-
-
- {{ .Data.FileContents }}
-
-
- {{ template "footer" }}
-
- {{ template "js" .}}
-
-
-{{- end -}}
diff --git a/gno.land/pkg/gnoweb/views/realm_help.html b/gno.land/pkg/gnoweb/views/realm_help.html
deleted file mode 100644
index b9c8e119e7a..00000000000
--- a/gno.land/pkg/gnoweb/views/realm_help.html
+++ /dev/null
@@ -1,89 +0,0 @@
-{{- define "app" -}}
-
-
-
- {{ template "html_head" . }}
- Gno.land - {{.Data.DirPath}}
-
-
-
-
-
-
-
-
-
- These are the realm's exposed functions ("public smart contracts").
-
- My address:
(see
`gnokey list` )
-
-
- {{ template "func_specs" . }}
-
-
- {{ template "footer" }}
-
- {{ template "js" . }}
-
-
-
-{{- end -}}
-
-{{- define "func_specs" -}}
-
- {{ $funcName := .Data.FuncName }} {{ $found := false }} {{ if eq $funcName "" }} {{ range .Data.FunctionSignatures }} {{ template "func_spec" . }} {{ end }} {{ else }} {{ range
- .Data.FunctionSignatures }} {{ if eq .FuncName $funcName }} {{ $found = true }} {{ template "func_spec" . }} {{ end }} {{ end }} {{ if not $found }} {{ $funcName }} not found. {{ end }} {{ end }}
-
-{{- end -}}
-
-{{- define "func_spec" -}}
-
-
-
- contract
- {{ .FuncName }}(...)
-
-
- params
-
-
- {{ range .Params }}{{ template "func_param" . }}{{ end }}
-
-
-
-
- results
-
-
- {{ range .Results }}{{ template "func_result" . }}{{ end }}
-
-
-
-
- command
-
-
-
-
-
-
-{{- end -}}
-
-{{- define "func_param" -}}
-
- {{ .Name }}
-
-
-
- {{ .Type }}
-
-{{- end -}}
-
-{{- define "func_result" -}}
-
- {{ .Name }}
- {{ .Type }}
-
-{{ end }}
diff --git a/gno.land/pkg/gnoweb/views/realm_render.html b/gno.land/pkg/gnoweb/views/realm_render.html
deleted file mode 100644
index 924ef2b414f..00000000000
--- a/gno.land/pkg/gnoweb/views/realm_render.html
+++ /dev/null
@@ -1,35 +0,0 @@
-{{- define "app" -}}
-
-
-
- {{ template "html_head" . }}
- Gno.land - {{.Data.RealmName}}
-
-
-
-
-
-
-
- {{ template "footer" }}
-
- {{ template "js" .}}
-
-
-{{- end -}}
diff --git a/gno.land/pkg/gnoweb/views/redirect.html b/gno.land/pkg/gnoweb/views/redirect.html
deleted file mode 100644
index 6fe43a7138b..00000000000
--- a/gno.land/pkg/gnoweb/views/redirect.html
+++ /dev/null
@@ -1,16 +0,0 @@
-{{- define "app" -}}
-
-
-
-
-
-
-
- Redirecting to {{.Data.To}}
-
-
- {{.Data.To}}
- {{ template "analytics" .}}
-
-
-{{- end -}}
diff --git a/gno.land/pkg/gnoweb/webclient.go b/gno.land/pkg/gnoweb/webclient.go
new file mode 100644
index 00000000000..de44303f352
--- /dev/null
+++ b/gno.land/pkg/gnoweb/webclient.go
@@ -0,0 +1,45 @@
+package gnoweb
+
+import (
+ "errors"
+ "io"
+
+ md "github.com/gnolang/gno/gno.land/pkg/gnoweb/markdown"
+ "github.com/gnolang/gno/gno.land/pkg/sdk/vm"
+)
+
+var (
+ ErrClientPathNotFound = errors.New("package not found")
+ ErrClientBadRequest = errors.New("bad request")
+ ErrClientResponse = errors.New("node response error")
+)
+
+type FileMeta struct {
+ Lines int
+ SizeKb float64
+}
+
+type RealmMeta struct {
+ Toc md.Toc
+}
+
+// WebClient is an interface for interacting with package and node ressources.
+type WebClient interface {
+ // RenderRealm renders the content of a realm from a given path and
+ // arguments into the giver `writer`. The method should ensures the rendered
+ // content is safely handled and formatted.
+ RenderRealm(w io.Writer, path string, args string) (*RealmMeta, error)
+
+ // SourceFile fetches and writes the source file from a given
+ // package path and file name. The method should ensures the source
+ // file's content is safely handled and formatted.
+ SourceFile(w io.Writer, pkgPath, fileName string) (*FileMeta, error)
+
+ // Functions retrieves a list of function signatures from a
+ // specified package path.
+ Functions(path string) ([]vm.FunctionSignature, error)
+
+ // Sources lists all source files available in a specified
+ // package path.
+ Sources(path string) ([]string, error)
+}
diff --git a/gno.land/pkg/gnoweb/webclient_html.go b/gno.land/pkg/gnoweb/webclient_html.go
new file mode 100644
index 00000000000..ffe2238df98
--- /dev/null
+++ b/gno.land/pkg/gnoweb/webclient_html.go
@@ -0,0 +1,190 @@
+package gnoweb
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "log/slog"
+ "path/filepath"
+ "strings"
+
+ md "github.com/gnolang/gno/gno.land/pkg/gnoweb/markdown"
+ "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // for error types
+ "github.com/gnolang/gno/tm2/pkg/amino"
+ "github.com/gnolang/gno/tm2/pkg/bft/rpc/client"
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/text"
+)
+
+type HTMLWebClientConfig struct {
+ Domain string
+ UnsafeHTML bool
+ RPCClient *client.RPCClient
+ Highlighter FormatSource
+ Markdown goldmark.Markdown
+}
+
+// NewDefaultHTMLWebClientConfig initializes a WebClientConfig with default settings.
+// It sets up goldmark Markdown parsing options and default domain and highlighter.
+func NewDefaultHTMLWebClientConfig(client *client.RPCClient) *HTMLWebClientConfig {
+ mdopts := []goldmark.Option{goldmark.WithParserOptions(parser.WithAutoHeadingID())}
+ return &HTMLWebClientConfig{
+ Domain: "gno.land",
+ Highlighter: &noopFormat{},
+ Markdown: goldmark.New(mdopts...),
+ RPCClient: client,
+ }
+}
+
+type HTMLWebClient struct {
+ domain string
+ logger *slog.Logger
+ client *client.RPCClient
+ md goldmark.Markdown
+ highlighter FormatSource
+}
+
+// NewHTMLClient creates a new instance of WebClient.
+// It requires a configured logger and WebClientConfig.
+func NewHTMLClient(log *slog.Logger, cfg *HTMLWebClientConfig) *HTMLWebClient {
+ return &HTMLWebClient{
+ logger: log,
+ domain: cfg.Domain,
+ client: cfg.RPCClient,
+ md: cfg.Markdown,
+ highlighter: cfg.Highlighter,
+ }
+}
+
+// Functions retrieves a list of function signatures from a
+// specified package path.
+func (s *HTMLWebClient) Functions(pkgPath string) ([]vm.FunctionSignature, error) {
+ const qpath = "vm/qfuncs"
+
+ args := fmt.Sprintf("%s/%s", s.domain, strings.Trim(pkgPath, "/"))
+ res, err := s.query(qpath, []byte(args))
+ if err != nil {
+ return nil, fmt.Errorf("unable to query func list: %w", err)
+ }
+
+ var fsigs vm.FunctionSignatures
+ if err := amino.UnmarshalJSON(res, &fsigs); err != nil {
+ s.logger.Warn("unable to unmarshal function signatures, client is probably outdated")
+ return nil, fmt.Errorf("unable to unmarshal function signatures: %w", err)
+ }
+
+ return fsigs, nil
+}
+
+// SourceFile fetches and writes the source file from a given
+// package path and file name to the provided writer. It uses
+// Chroma for syntax highlighting source.
+func (s *HTMLWebClient) SourceFile(w io.Writer, path, fileName string) (*FileMeta, error) {
+ const qpath = "vm/qfile"
+
+ fileName = strings.TrimSpace(fileName)
+ if fileName == "" {
+ return nil, errors.New("empty filename given") // XXX: Consider creating a specific error variable
+ }
+
+ // XXX: Consider moving this into gnoclient
+ fullPath := filepath.Join(s.domain, strings.Trim(path, "/"), fileName)
+
+ source, err := s.query(qpath, []byte(fullPath))
+ if err != nil {
+ // XXX: this is a bit ugly, we should make the keeper return an
+ // assertable error.
+ if strings.Contains(err.Error(), "not available") {
+ return nil, ErrClientPathNotFound
+ }
+
+ return nil, err
+ }
+
+ fileMeta := FileMeta{
+ Lines: strings.Count(string(source), "\n"),
+ SizeKb: float64(len(source)) / 1024.0,
+ }
+
+ // Use Chroma for syntax highlighting
+ if err := s.highlighter.Format(w, fileName, source); err != nil {
+ return nil, err
+ }
+
+ return &fileMeta, nil
+}
+
+// Sources lists all source files available in a specified
+// package path by querying the RPC client.
+func (s *HTMLWebClient) Sources(path string) ([]string, error) {
+ const qpath = "vm/qfile"
+
+ // XXX: Consider moving this into gnoclient
+ pkgPath := strings.Trim(path, "/")
+ fullPath := fmt.Sprintf("%s/%s", s.domain, pkgPath)
+ res, err := s.query(qpath, []byte(fullPath))
+ if err != nil {
+ // XXX: this is a bit ugly, we should make the keeper return an
+ // assertable error.
+ if strings.Contains(err.Error(), "not available") {
+ return nil, ErrClientPathNotFound
+ }
+
+ return nil, err
+ }
+
+ files := strings.Split(strings.TrimSpace(string(res)), "\n")
+ return files, nil
+}
+
+// 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.
+func (s *HTMLWebClient) RenderRealm(w io.Writer, pkgPath string, args string) (*RealmMeta, error) {
+ const qpath = "vm/qrender"
+
+ pkgPath = strings.Trim(pkgPath, "/")
+ data := fmt.Sprintf("%s/%s:%s", s.domain, pkgPath, args)
+ rawres, err := s.query(qpath, []byte(data))
+ if err != nil {
+ return nil, err
+ }
+
+ // Use Goldmark for Markdown parsing
+ doc := s.md.Parser().Parse(text.NewReader(rawres))
+ if err := s.md.Renderer().Render(w, rawres, doc); err != nil {
+ return nil, fmt.Errorf("unable to render realm %q: %w", data, err)
+ }
+
+ var meta RealmMeta
+ meta.Toc, err = md.TocInspect(doc, rawres, md.TocOptions{MaxDepth: 6, MinDepth: 2})
+ if err != nil {
+ s.logger.Warn("unable to inspect for TOC elements", "error", err)
+ }
+
+ return &meta, nil
+}
+
+// query sends a query to the RPC client and returns the response
+// data.
+func (s *HTMLWebClient) query(qpath string, data []byte) ([]byte, error) {
+ s.logger.Info("query", "path", qpath, "data", string(data))
+
+ qres, err := s.client.ABCIQuery(qpath, data)
+ if err != nil {
+ s.logger.Debug("request error", "path", qpath, "data", string(data), "error", err)
+ return nil, fmt.Errorf("%w: %s", ErrClientBadRequest, err.Error())
+ }
+
+ if err = qres.Response.Error; err != nil {
+ if errors.Is(err, vm.InvalidPkgPathError{}) {
+ return nil, ErrClientPathNotFound
+ }
+
+ s.logger.Error("response error", "path", qpath, "log", qres.Response.Log)
+ return nil, fmt.Errorf("%w: %s", ErrClientResponse, err.Error())
+ }
+
+ return qres.Response.Data, nil
+}
diff --git a/gno.land/pkg/gnoweb/webclient_mock.go b/gno.land/pkg/gnoweb/webclient_mock.go
new file mode 100644
index 00000000000..451f5e237c3
--- /dev/null
+++ b/gno.land/pkg/gnoweb/webclient_mock.go
@@ -0,0 +1,91 @@
+package gnoweb
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "sort"
+
+ "github.com/gnolang/gno/gno.land/pkg/sdk/vm"
+)
+
+// MockPackage represents a mock package with files and function signatures.
+type MockPackage struct {
+ Path string
+ Domain string
+ Files map[string]string // filename -> body
+ Functions []vm.FunctionSignature
+}
+
+// MockWebClient is a mock implementation of the Client interface.
+type MockWebClient struct {
+ Packages map[string]*MockPackage // path -> package
+}
+
+func NewMockWebClient(pkgs ...*MockPackage) *MockWebClient {
+ mpkgs := make(map[string]*MockPackage)
+ for _, pkg := range pkgs {
+ mpkgs[pkg.Path] = pkg
+ }
+
+ return &MockWebClient{Packages: mpkgs}
+}
+
+// Render simulates rendering a package by writing its content to the writer.
+func (m *MockWebClient) RenderRealm(w io.Writer, path string, args string) (*RealmMeta, error) {
+ pkg, exists := m.Packages[path]
+ if !exists {
+ return nil, ErrClientPathNotFound
+ }
+
+ fmt.Fprintf(w, "[%s]%s:", pkg.Domain, pkg.Path)
+
+ // Return a dummy RealmMeta for simplicity
+ return &RealmMeta{}, nil
+}
+
+// SourceFile simulates retrieving a source file's metadata.
+func (m *MockWebClient) SourceFile(w io.Writer, pkgPath, fileName string) (*FileMeta, error) {
+ pkg, exists := m.Packages[pkgPath]
+ if !exists {
+ return nil, ErrClientPathNotFound
+ }
+
+ if body, ok := pkg.Files[fileName]; ok {
+ w.Write([]byte(body))
+ return &FileMeta{
+ Lines: len(bytes.Split([]byte(body), []byte("\n"))),
+ SizeKb: float64(len(body)) / 1024.0,
+ }, nil
+ }
+
+ return nil, ErrClientPathNotFound
+}
+
+// Functions simulates retrieving function signatures from a package.
+func (m *MockWebClient) Functions(path string) ([]vm.FunctionSignature, error) {
+ pkg, exists := m.Packages[path]
+ if !exists {
+ return nil, ErrClientPathNotFound
+ }
+
+ return pkg.Functions, nil
+}
+
+// Sources simulates listing all source files in a package.
+func (m *MockWebClient) Sources(path string) ([]string, error) {
+ pkg, exists := m.Packages[path]
+ if !exists {
+ return nil, ErrClientPathNotFound
+ }
+
+ fileNames := make([]string, 0, len(pkg.Files))
+ for file := range pkg.Files {
+ fileNames = append(fileNames, file)
+ }
+
+ // Sort for consistency
+ sort.Strings(fileNames)
+
+ return fileNames, nil
+}
diff --git a/gno.land/pkg/integration/doc.go b/gno.land/pkg/integration/doc.go
index 2b6d24c23b8..d93d4607a59 100644
--- a/gno.land/pkg/integration/doc.go
+++ b/gno.land/pkg/integration/doc.go
@@ -8,9 +8,12 @@
//
// Additional Command Overview:
//
-// 1. `gnoland [start|stop]`:
+// 1. `gnoland [start|stop|restart]`:
// - The gnoland node doesn't start automatically. This enables the user to do some
// pre-configuration or pass custom arguments to the start command.
+// - `gnoland restart` will simulate restarting a node, as in stopping and
+// starting it again, recovering state from the persisted database data.
+// - `gnoland start -non-validator` can be used to start a node as a non-validator node.
//
// 2. `gnokey`:
// - Supports most of the common commands.
@@ -73,13 +76,6 @@
//
// Input:
//
-// - LOG_LEVEL:
-// The logging level to be used, which can be one of "error", "debug", "info", or an empty string.
-// If empty, the log level defaults to "debug".
-//
-// - LOG_DIR:
-// If set, logs will be directed to the specified directory.
-//
// - TESTWORK:
// A boolean that, when enabled, retains working directories after tests for
// inspection. If enabled, gnoland logs will be persisted inside this
@@ -106,11 +102,17 @@
// The path where the gnoland node stores its configuration and data. It's
// set only if the node has started.
//
-// - USER_SEED_test1:
-// Contains the seed for the test1 account.
+// - xxx_user_seed:
+// Where `xxx` is the account name; Contains the seed for the test1 account.
+//
+// - xxx_user_addr:
+// Where `xxx` is the account name; Contains the address for the test1 account.
+//
+// - xxx_account_num:
+// Where `xxx` is the account name; Contains the account number for the test1 account.
//
-// - USER_ADDR_test1:
-// Contains the address for the test1 account.
+// - xxx_account_seq:
+// Where `xxx` is the account name; Contains the address for the test1 account.
//
// - RPC_ADDR:
// Points to the gnoland node's remote address. It's set only if the node has started.
diff --git a/gno.land/pkg/integration/integration_test.go b/gno.land/pkg/integration/integration_test.go
deleted file mode 100644
index 99a3e6c7eca..00000000000
--- a/gno.land/pkg/integration/integration_test.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package integration
-
-import (
- "strings"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestTestdata(t *testing.T) {
- t.Parallel()
-
- RunGnolandTestscripts(t, "testdata")
-}
-
-func TestUnquote(t *testing.T) {
- t.Parallel()
-
- cases := []struct {
- Input string
- Expected []string
- ShouldFail bool
- }{
- {"", []string{""}, false},
- {"g", []string{"g"}, false},
- {"Hello Gno", []string{"Hello", "Gno"}, false},
- {`"Hello" "Gno"`, []string{"Hello", "Gno"}, false},
- {`"Hel lo" "Gno"`, []string{"Hel lo", "Gno"}, false},
- {`"H e l l o\n" \nGno`, []string{"H e l l o\n", "\\nGno"}, false},
- {`"Hel\n"\nlo " ""G"n"o"`, []string{"Hel\n\\nlo", " Gno"}, false},
- {`"He said, \"Hello\"" "Gno"`, []string{`He said, "Hello"`, "Gno"}, false},
- {`"\n \t" \n\t`, []string{"\n \t", "\\n\\t"}, false},
- {`"Hel\\n"\t\\nlo " ""\\nGno"`, []string{"Hel\\n\\t\\\\nlo", " \\nGno"}, false},
- // errors:
- {`"Hello Gno`, []string{}, true}, // unfinished quote
- {`"Hello\e Gno"`, []string{}, true}, // unhandled escape sequence
- }
-
- for _, tc := range cases {
- tc := tc
- t.Run(tc.Input, func(t *testing.T) {
- t.Parallel()
-
- // split by whitespace to simulate command-line arguments
- args := strings.Split(tc.Input, " ")
- unquotedArgs, err := unquote(args)
- if tc.ShouldFail {
- require.Error(t, err)
- return
- }
-
- require.NoError(t, err)
- assert.Equal(t, tc.Expected, unquotedArgs)
- })
- }
-}
diff --git a/gno.land/pkg/integration/node_testing.go b/gno.land/pkg/integration/node_testing.go
new file mode 100644
index 00000000000..edcf53de5d3
--- /dev/null
+++ b/gno.land/pkg/integration/node_testing.go
@@ -0,0 +1,188 @@
+package integration
+
+import (
+ "log/slog"
+ "path/filepath"
+ "slices"
+ "time"
+
+ "github.com/gnolang/gno/gno.land/pkg/gnoland"
+ "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
+ abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
+ tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config"
+ "github.com/gnolang/gno/tm2/pkg/bft/node"
+ bft "github.com/gnolang/gno/tm2/pkg/bft/types"
+ "github.com/gnolang/gno/tm2/pkg/crypto"
+ "github.com/gnolang/gno/tm2/pkg/db/memdb"
+ "github.com/gnolang/gno/tm2/pkg/std"
+ "github.com/stretchr/testify/require"
+)
+
+const (
+ DefaultAccount_Name = "test1"
+ DefaultAccount_Address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"
+ DefaultAccount_Seed = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast"
+)
+
+// TestingInMemoryNode initializes and starts an in-memory node for testing.
+// It returns the node instance and its RPC remote address.
+func TestingInMemoryNode(t TestingTS, logger *slog.Logger, config *gnoland.InMemoryNodeConfig) (*node.Node, string) {
+ node, err := gnoland.NewInMemoryNode(logger, config)
+ require.NoError(t, err)
+
+ err = node.Start()
+ require.NoError(t, err)
+
+ ourAddress := config.PrivValidator.GetPubKey().Address()
+ isValidator := slices.ContainsFunc(config.Genesis.Validators, func(val bft.GenesisValidator) bool {
+ return val.Address == ourAddress
+ })
+
+ // Wait for first block if we are a validator.
+ // If we are not a validator, we don't produce blocks, so node.Ready() hangs.
+ if isValidator {
+ select {
+ case <-node.Ready():
+ case <-time.After(time.Second * 10):
+ require.FailNow(t, "timeout while waiting for the node to start")
+ }
+ }
+
+ return node, node.Config().RPC.ListenAddress
+}
+
+// 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, additionalTxs ...gnoland.TxWithMetadata) (*gnoland.InMemoryNodeConfig, bft.Address) {
+ cfg := TestingMinimalNodeConfig(gnoroot)
+ cfg.SkipGenesisVerification = true
+
+ creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1
+
+ params := LoadDefaultGenesisParamFile(t, gnoroot)
+ balances := LoadDefaultGenesisBalanceFile(t, gnoroot)
+ txs := make([]gnoland.TxWithMetadata, 0)
+ txs = append(txs, LoadDefaultPackages(t, creator, gnoroot)...)
+ txs = append(txs, additionalTxs...)
+
+ cfg.Genesis.AppState = gnoland.GnoGenesisState{
+ Balances: balances,
+ Txs: txs,
+ Params: params,
+ }
+
+ return cfg, creator
+}
+
+// TestingMinimalNodeConfig constructs the default minimal in-memory node configuration for testing.
+func TestingMinimalNodeConfig(gnoroot string) *gnoland.InMemoryNodeConfig {
+ tmconfig := DefaultTestingTMConfig(gnoroot)
+
+ // Create Mocked Identity
+ pv := gnoland.NewMockedPrivValidator()
+
+ // Generate genesis config
+ genesis := DefaultTestingGenesisConfig(gnoroot, pv.GetPubKey(), tmconfig)
+
+ return &gnoland.InMemoryNodeConfig{
+ PrivValidator: pv,
+ Genesis: genesis,
+ TMConfig: tmconfig,
+ DB: memdb.NewMemDB(),
+ InitChainerConfig: gnoland.InitChainerConfig{
+ GenesisTxResultHandler: gnoland.PanicOnFailingTxResultHandler,
+ CacheStdlibLoad: true,
+ },
+ }
+}
+
+func DefaultTestingGenesisConfig(gnoroot string, self crypto.PubKey, tmconfig *tmcfg.Config) *bft.GenesisDoc {
+ return &bft.GenesisDoc{
+ GenesisTime: time.Now(),
+ ChainID: tmconfig.ChainID(),
+ ConsensusParams: abci.ConsensusParams{
+ Block: &abci.BlockParams{
+ MaxTxBytes: 1_000_000, // 1MB,
+ MaxDataBytes: 2_000_000, // 2MB,
+ MaxGas: 100_000_000, // 100M gas
+ TimeIotaMS: 100, // 100ms
+ },
+ },
+ Validators: []bft.GenesisValidator{
+ {
+ Address: self.Address(),
+ PubKey: self,
+ Power: 10,
+ Name: "self",
+ },
+ },
+ AppState: gnoland.GnoGenesisState{
+ Balances: []gnoland.Balance{
+ {
+ Address: crypto.MustAddressFromString(DefaultAccount_Address),
+ Amount: std.MustParseCoins(ugnot.ValueString(10_000_000_000_000)),
+ },
+ },
+ Txs: []gnoland.TxWithMetadata{},
+ Params: []gnoland.Param{},
+ },
+ }
+}
+
+// LoadDefaultPackages loads the default packages for testing using a given creator address and gnoroot directory.
+func LoadDefaultPackages(t TestingTS, creator bft.Address, gnoroot string) []gnoland.TxWithMetadata {
+ examplesDir := filepath.Join(gnoroot, "examples")
+
+ defaultFee := std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000)))
+ txs, err := gnoland.LoadPackagesFromDir(examplesDir, creator, defaultFee)
+ require.NoError(t, err)
+
+ return txs
+}
+
+// LoadDefaultGenesisBalanceFile loads the default genesis balance file for testing.
+func LoadDefaultGenesisBalanceFile(t TestingTS, gnoroot string) []gnoland.Balance {
+ balanceFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_balances.txt")
+
+ genesisBalances, err := gnoland.LoadGenesisBalancesFile(balanceFile)
+ require.NoError(t, err)
+
+ return genesisBalances.List()
+}
+
+// LoadDefaultGenesisParamFile loads the default genesis balance file for testing.
+func LoadDefaultGenesisParamFile(t TestingTS, gnoroot string) []gnoland.Param {
+ paramFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_params.toml")
+
+ genesisParams, err := gnoland.LoadGenesisParamsFile(paramFile)
+ require.NoError(t, err)
+
+ return genesisParams
+}
+
+// LoadDefaultGenesisTXsFile loads the default genesis transactions file for testing.
+func LoadDefaultGenesisTXsFile(t TestingTS, chainid string, gnoroot string) []gnoland.TxWithMetadata {
+ txsFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_txs.jsonl")
+
+ // NOTE: We dont care about giving a correct address here, as it's only for display
+ // XXX: Do we care loading this TXs for testing ?
+ genesisTXs, err := gnoland.LoadGenesisTxsFile(txsFile, chainid, "https://127.0.0.1:26657")
+ require.NoError(t, err)
+
+ return genesisTXs
+}
+
+// DefaultTestingTMConfig constructs the default Tendermint configuration for testing.
+func DefaultTestingTMConfig(gnoroot string) *tmcfg.Config {
+ const defaultListner = "tcp://127.0.0.1:0"
+
+ tmconfig := tmcfg.TestConfig().SetRootDir(gnoroot)
+ tmconfig.Consensus.WALDisabled = true
+ tmconfig.Consensus.SkipTimeoutCommit = true
+ tmconfig.Consensus.CreateEmptyBlocks = true
+ tmconfig.Consensus.CreateEmptyBlocksInterval = time.Millisecond * 100
+ tmconfig.RPC.ListenAddress = defaultListner
+ tmconfig.P2P.ListenAddress = defaultListner
+ return tmconfig
+}
diff --git a/gno.land/pkg/integration/pkgloader.go b/gno.land/pkg/integration/pkgloader.go
new file mode 100644
index 00000000000..71b1491b2a8
--- /dev/null
+++ b/gno.land/pkg/integration/pkgloader.go
@@ -0,0 +1,173 @@
+package integration
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ "github.com/gnolang/gno/gno.land/pkg/gnoland"
+ "github.com/gnolang/gno/gno.land/pkg/sdk/vm"
+ "github.com/gnolang/gno/gnovm/pkg/gnolang"
+ "github.com/gnolang/gno/gnovm/pkg/gnomod"
+ "github.com/gnolang/gno/gnovm/pkg/packages"
+ "github.com/gnolang/gno/tm2/pkg/crypto"
+ "github.com/gnolang/gno/tm2/pkg/std"
+)
+
+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),
+ }
+}
+
+func (pl *PkgsLoader) List() gnomod.PkgList {
+ return pl.pkgs
+}
+
+func (pl *PkgsLoader) SetPatch(replace, with string) {
+ pl.patchs[replace] = with
+}
+
+func (pl *PkgsLoader) LoadPackages(creatorKey crypto.PrivKey, fee std.Fee, deposit std.Coins) ([]gnoland.TxWithMetadata, error) {
+ pkgslist, err := pl.List().Sort() // sorts packages by their dependencies.
+ if err != nil {
+ return nil, fmt.Errorf("unable to sort packages: %w", err)
+ }
+
+ txs := make([]gnoland.TxWithMetadata, len(pkgslist))
+ for i, pkg := range pkgslist {
+ tx, err := gnoland.LoadPackage(pkg, creatorKey.PubKey().Address(), fee, deposit)
+ 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] = gnoland.TxWithMetadata{
+ Tx: tx,
+ }
+ }
+
+ if err = gnoland.SignGenesisTxs(txs, creatorKey, "tendermint_test"); err != nil {
+ return nil, fmt.Errorf("unable to sign txs: %w", err)
+ }
+
+ return txs, nil
+}
+
+func (pl *PkgsLoader) LoadAllPackagesFromDir(path string) error {
+ // list all packages from target path
+ pkgslist, err := gnomod.ListPkgs(path)
+ if err != nil {
+ return fmt.Errorf("listing gno packages: %w", err)
+ }
+
+ for _, pkg := range pkgslist {
+ if !pl.exist(pkg) {
+ pl.add(pkg)
+ }
+ }
+
+ return nil
+}
+
+func (pl *PkgsLoader) LoadPackage(modroot string, path, name string) error {
+ // Initialize a queue with the root package
+ queue := []gnomod.Pkg{{Dir: path, Name: name}}
+
+ for len(queue) > 0 {
+ // Dequeue the first package
+ currentPkg := queue[0]
+ queue = queue[1:]
+
+ if currentPkg.Dir == "" {
+ return fmt.Errorf("no path specified for package")
+ }
+
+ if currentPkg.Name == "" {
+ // Load `gno.mod` information
+ gnoModPath := filepath.Join(currentPkg.Dir, "gno.mod")
+ gm, err := gnomod.ParseGnoMod(gnoModPath)
+ if err != nil {
+ return fmt.Errorf("unable to load %q: %w", gnoModPath, err)
+ }
+ gm.Sanitize()
+
+ // Override package info with mod infos
+ currentPkg.Name = gm.Module.Mod.Path
+ currentPkg.Draft = gm.Draft
+
+ pkg, err := gnolang.ReadMemPackage(currentPkg.Dir, currentPkg.Name)
+ if err != nil {
+ return fmt.Errorf("unable to read package at %q: %w", currentPkg.Dir, err)
+ }
+ importsMap, err := packages.Imports(pkg, nil)
+ if err != nil {
+ return fmt.Errorf("unable to load package imports in %q: %w", currentPkg.Dir, err)
+ }
+ imports := importsMap.Merge(packages.FileKindPackageSource, packages.FileKindTest, packages.FileKindFiletest)
+ for _, imp := range imports {
+ if imp.PkgPath == currentPkg.Name || gnolang.IsStdlib(imp.PkgPath) {
+ continue
+ }
+ currentPkg.Imports = append(currentPkg.Imports, imp.PkgPath)
+ }
+ }
+
+ if currentPkg.Draft {
+ continue // Skip draft package
+ }
+
+ if pl.exist(currentPkg) {
+ continue
+ }
+ pl.add(currentPkg)
+
+ // Add requirements to the queue
+ for _, pkgPath := range currentPkg.Imports {
+ fullPath := filepath.Join(modroot, pkgPath)
+ queue = append(queue, gnomod.Pkg{Dir: fullPath})
+ }
+ }
+
+ return nil
+}
+
+func (pl *PkgsLoader) add(pkg gnomod.Pkg) {
+ pl.visited[pkg.Name] = struct{}{}
+ pl.pkgs = append(pl.pkgs, pkg)
+}
+
+func (pl *PkgsLoader) exist(pkg gnomod.Pkg) (ok bool) {
+ _, ok = pl.visited[pkg.Name]
+ return
+}
diff --git a/gno.land/pkg/integration/process.go b/gno.land/pkg/integration/process.go
new file mode 100644
index 00000000000..839004ca1f3
--- /dev/null
+++ b/gno.land/pkg/integration/process.go
@@ -0,0 +1,451 @@
+package integration
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "log/slog"
+ "os"
+ "os/exec"
+ "os/signal"
+ "slices"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/gnolang/gno/gno.land/pkg/gnoland"
+ "github.com/gnolang/gno/tm2/pkg/amino"
+ tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config"
+ bft "github.com/gnolang/gno/tm2/pkg/bft/types"
+ "github.com/gnolang/gno/tm2/pkg/crypto/ed25519"
+ "github.com/gnolang/gno/tm2/pkg/db"
+ "github.com/gnolang/gno/tm2/pkg/db/goleveldb"
+ "github.com/gnolang/gno/tm2/pkg/db/memdb"
+ "github.com/stretchr/testify/require"
+)
+
+const gracefulShutdown = time.Second * 5
+
+type ProcessNodeConfig struct {
+ ValidatorKey ed25519.PrivKeyEd25519 `json:"priv"`
+ Verbose bool `json:"verbose"`
+ DBDir string `json:"dbdir"`
+ RootDir string `json:"rootdir"`
+ Genesis *MarshalableGenesisDoc `json:"genesis"`
+ TMConfig *tmcfg.Config `json:"tm"`
+}
+
+type ProcessConfig struct {
+ Node *ProcessNodeConfig
+
+ // These parameters are not meant to be passed to the process
+ CoverDir string
+ Stderr, Stdout io.Writer
+}
+
+func (i ProcessConfig) validate() error {
+ if i.Node.TMConfig == nil {
+ return errors.New("no tm config set")
+ }
+
+ if i.Node.Genesis == nil {
+ return errors.New("no genesis is set")
+ }
+
+ return nil
+}
+
+// RunNode initializes and runs a gnoaland node with the provided configuration.
+func RunNode(ctx context.Context, pcfg *ProcessNodeConfig, stdout, stderr io.Writer) error {
+ // Setup logger based on verbosity
+ var handler slog.Handler
+ if pcfg.Verbose {
+ handler = slog.NewTextHandler(stdout, &slog.HandlerOptions{Level: slog.LevelDebug})
+ } else {
+ handler = slog.NewTextHandler(stdout, &slog.HandlerOptions{Level: slog.LevelError})
+ }
+ logger := slog.New(handler)
+
+ // Initialize database
+ db, err := initDatabase(pcfg.DBDir)
+ if err != nil {
+ return err
+ }
+ defer db.Close() // ensure db is close
+
+ nodecfg := TestingMinimalNodeConfig(pcfg.RootDir)
+
+ // Configure validator if provided
+ if len(pcfg.ValidatorKey) > 0 && !isAllZero(pcfg.ValidatorKey) {
+ nodecfg.PrivValidator = bft.NewMockPVWithParams(pcfg.ValidatorKey, false, false)
+ }
+ pv := nodecfg.PrivValidator.GetPubKey()
+
+ // Setup node configuration
+ nodecfg.DB = db
+ nodecfg.TMConfig.DBPath = pcfg.DBDir
+ nodecfg.TMConfig = pcfg.TMConfig
+ nodecfg.Genesis = pcfg.Genesis.ToGenesisDoc()
+ nodecfg.Genesis.Validators = []bft.GenesisValidator{
+ {
+ Address: pv.Address(),
+ PubKey: pv,
+ Power: 10,
+ Name: "self",
+ },
+ }
+
+ // Create and start the node
+ node, err := gnoland.NewInMemoryNode(logger, nodecfg)
+ if err != nil {
+ return fmt.Errorf("failed to create new in-memory node: %w", err)
+ }
+
+ if err := node.Start(); err != nil {
+ return fmt.Errorf("failed to start node: %w", err)
+ }
+ defer node.Stop()
+
+ // Determine if the node is a validator
+ ourAddress := nodecfg.PrivValidator.GetPubKey().Address()
+ isValidator := slices.ContainsFunc(nodecfg.Genesis.Validators, func(val bft.GenesisValidator) bool {
+ return val.Address == ourAddress
+ })
+
+ lisnAddress := node.Config().RPC.ListenAddress
+ if isValidator {
+ select {
+ case <-ctx.Done():
+ return fmt.Errorf("waiting for the node to start: %w", ctx.Err())
+ case <-node.Ready():
+ }
+ }
+
+ // Write READY signal to stdout
+ signalWriteReady(stdout, lisnAddress)
+
+ <-ctx.Done()
+ return node.Stop()
+}
+
+type NodeProcess interface {
+ Stop() error
+ Address() string
+}
+
+type nodeProcess struct {
+ cmd *exec.Cmd
+ address string
+
+ stopOnce sync.Once
+ stopErr error
+}
+
+func (n *nodeProcess) Address() string {
+ return n.address
+}
+
+func (n *nodeProcess) Stop() error {
+ n.stopOnce.Do(func() {
+ // Send SIGTERM to the process
+ if err := n.cmd.Process.Signal(os.Interrupt); err != nil {
+ n.stopErr = fmt.Errorf("error sending `SIGINT` to the node: %w", err)
+ return
+ }
+
+ // Optionally wait for the process to exit
+ if _, err := n.cmd.Process.Wait(); err != nil {
+ n.stopErr = fmt.Errorf("process exited with error: %w", err)
+ return
+ }
+ })
+
+ return n.stopErr
+}
+
+// RunNodeProcess runs the binary at the given path with the provided configuration.
+func RunNodeProcess(ctx context.Context, cfg ProcessConfig, name string, args ...string) (NodeProcess, error) {
+ if cfg.Stdout == nil {
+ cfg.Stdout = os.Stdout
+ }
+
+ if cfg.Stderr == nil {
+ cfg.Stderr = os.Stderr
+ }
+
+ if err := cfg.validate(); err != nil {
+ return nil, err
+ }
+
+ // Marshal the configuration to JSON
+ nodeConfigData, err := json.Marshal(cfg.Node)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal config to JSON: %w", err)
+ }
+
+ // Create and configure the command to execute the binary
+ cmd := exec.Command(name, args...)
+ cmd.Env = os.Environ()
+ cmd.Stdin = bytes.NewReader(nodeConfigData)
+
+ if cfg.CoverDir != "" {
+ cmd.Env = append(cmd.Env, "GOCOVERDIR="+cfg.CoverDir)
+ }
+
+ // Redirect all errors into a buffer
+ cmd.Stderr = os.Stderr
+ if cfg.Stderr != nil {
+ cmd.Stderr = cfg.Stderr
+ }
+
+ // Create pipes for stdout
+ stdoutPipe, err := cmd.StdoutPipe()
+ if err != nil {
+ return nil, fmt.Errorf("failed to create stdout pipe: %w", err)
+ }
+
+ // Start the command
+ if err := cmd.Start(); err != nil {
+ return nil, fmt.Errorf("failed to start command: %w", err)
+ }
+
+ address, err := waitForProcessReady(ctx, stdoutPipe, cfg.Stdout)
+ if err != nil {
+ return nil, fmt.Errorf("waiting for readiness: %w", err)
+ }
+
+ return &nodeProcess{
+ cmd: cmd,
+ address: address,
+ }, nil
+}
+
+type nodeInMemoryProcess struct {
+ address string
+
+ stopOnce sync.Once
+ stopErr error
+ stop context.CancelFunc
+ ccNodeError chan error
+}
+
+func (n *nodeInMemoryProcess) Address() string {
+ return n.address
+}
+
+func (n *nodeInMemoryProcess) Stop() error {
+ n.stopOnce.Do(func() {
+ n.stop()
+ var err error
+ select {
+ case err = <-n.ccNodeError:
+ case <-time.After(time.Second * 5):
+ err = fmt.Errorf("timeout while waiting for node to stop")
+ }
+
+ if err != nil {
+ n.stopErr = fmt.Errorf("unable to node gracefully: %w", err)
+ }
+ })
+
+ return n.stopErr
+}
+
+func RunInMemoryProcess(ctx context.Context, cfg ProcessConfig) (NodeProcess, error) {
+ ctx, cancel := context.WithCancel(ctx)
+
+ out, in := io.Pipe()
+ ccStopErr := make(chan error, 1)
+ go func() {
+ defer close(ccStopErr)
+ defer cancel()
+
+ err := RunNode(ctx, cfg.Node, in, cfg.Stderr)
+ if err != nil {
+ fmt.Fprintf(cfg.Stderr, "run node failed: %v", err)
+ }
+
+ ccStopErr <- err
+ }()
+
+ address, err := waitForProcessReady(ctx, out, cfg.Stdout)
+ if err == nil { // ok
+ return &nodeInMemoryProcess{
+ address: address,
+ stop: cancel,
+ ccNodeError: ccStopErr,
+ }, nil
+ }
+
+ cancel()
+
+ select {
+ case err = <-ccStopErr: // return node error in priority
+ default:
+ }
+
+ return nil, err
+}
+
+func RunMain(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer) error {
+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
+ defer stop()
+
+ // Read the configuration from standard input
+ configData, err := io.ReadAll(stdin)
+ if err != nil {
+ // log.Fatalf("error reading stdin: %v", err)
+ return fmt.Errorf("error reading stdin: %w", err)
+ }
+
+ // Unmarshal the JSON configuration
+ var cfg ProcessNodeConfig
+ if err := json.Unmarshal(configData, &cfg); err != nil {
+ return fmt.Errorf("error unmarshaling JSON: %w", err)
+ // log.Fatalf("error unmarshaling JSON: %v", err)
+ }
+
+ // Run the node
+ ccErr := make(chan error, 1)
+ go func() {
+ ccErr <- RunNode(ctx, &cfg, stdout, stderr)
+ close(ccErr)
+ }()
+
+ // Wait for the node to gracefully terminate
+ <-ctx.Done()
+
+ // Attempt graceful shutdown
+ select {
+ case <-time.After(gracefulShutdown):
+ return fmt.Errorf("unable to gracefully stop the node, exiting now")
+ case err = <-ccErr: // done
+ }
+
+ return err
+}
+
+func runTestingNodeProcess(t TestingTS, ctx context.Context, pcfg ProcessConfig) NodeProcess {
+ bin, err := os.Executable()
+ require.NoError(t, err)
+ args := []string{
+ "-test.run=^$",
+ "-run-node-process",
+ }
+
+ if pcfg.CoverDir != "" && testing.CoverMode() != "" {
+ args = append(args, "-test.gocoverdir="+pcfg.CoverDir)
+ }
+
+ node, err := RunNodeProcess(ctx, pcfg, bin, args...)
+ require.NoError(t, err)
+
+ return node
+}
+
+// initDatabase initializes the database based on the provided directory configuration.
+func initDatabase(dbDir string) (db.DB, error) {
+ if dbDir == "" {
+ return memdb.NewMemDB(), nil
+ }
+
+ data, err := goleveldb.NewGoLevelDB("testdb", dbDir)
+ if err != nil {
+ return nil, fmt.Errorf("unable to init database in %q: %w", dbDir, err)
+ }
+
+ return data, nil
+}
+
+func signalWriteReady(w io.Writer, address string) error {
+ _, err := fmt.Fprintf(w, "READY:%s\n", address)
+ return err
+}
+
+func signalReadReady(line string) (string, bool) {
+ var address string
+ if _, err := fmt.Sscanf(line, "READY:%s", &address); err == nil {
+ return address, true
+ }
+ return "", false
+}
+
+// waitForProcessReady waits for the process to signal readiness and returns the address.
+func waitForProcessReady(ctx context.Context, stdoutPipe io.Reader, out io.Writer) (string, error) {
+ var address string
+
+ cReady := make(chan error, 2)
+ go func() {
+ defer close(cReady)
+
+ scanner := bufio.NewScanner(stdoutPipe)
+ ready := false
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ if !ready {
+ if addr, ok := signalReadReady(line); ok {
+ address = addr
+ ready = true
+ cReady <- nil
+ }
+ }
+
+ fmt.Fprintln(out, line)
+ }
+
+ if err := scanner.Err(); err != nil {
+ cReady <- fmt.Errorf("error reading stdout: %w", err)
+ } else {
+ cReady <- fmt.Errorf("process exited without 'READY'")
+ }
+ }()
+
+ select {
+ case err := <-cReady:
+ return address, err
+ case <-ctx.Done():
+ return "", ctx.Err()
+ }
+}
+
+// isAllZero checks if a 64-byte key consists entirely of zeros.
+func isAllZero(key [64]byte) bool {
+ for _, v := range key {
+ if v != 0 {
+ return false
+ }
+ }
+ return true
+}
+
+type MarshalableGenesisDoc bft.GenesisDoc
+
+func NewMarshalableGenesisDoc(doc *bft.GenesisDoc) *MarshalableGenesisDoc {
+ m := MarshalableGenesisDoc(*doc)
+ return &m
+}
+
+func (m *MarshalableGenesisDoc) MarshalJSON() ([]byte, error) {
+ doc := (*bft.GenesisDoc)(m)
+ return amino.MarshalJSON(doc)
+}
+
+func (m *MarshalableGenesisDoc) UnmarshalJSON(data []byte) (err error) {
+ doc, err := bft.GenesisDocFromJSON(data)
+ if err != nil {
+ return err
+ }
+
+ *m = MarshalableGenesisDoc(*doc)
+ return
+}
+
+// Cast back to the original bft.GenesisDoc.
+func (m *MarshalableGenesisDoc) ToGenesisDoc() *bft.GenesisDoc {
+ return (*bft.GenesisDoc)(m)
+}
diff --git a/gno.land/pkg/integration/process/main.go b/gno.land/pkg/integration/process/main.go
new file mode 100644
index 00000000000..bcd52e6fd44
--- /dev/null
+++ b/gno.land/pkg/integration/process/main.go
@@ -0,0 +1,20 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/gnolang/gno/gno.land/pkg/integration"
+)
+
+func main() {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+ defer cancel()
+
+ if err := integration.RunMain(ctx, os.Stdin, os.Stdout, os.Stderr); err != nil {
+ fmt.Fprintln(os.Stderr, err.Error())
+ os.Exit(1)
+ }
+}
diff --git a/gno.land/pkg/integration/process_test.go b/gno.land/pkg/integration/process_test.go
new file mode 100644
index 00000000000..b8768ad0e63
--- /dev/null
+++ b/gno.land/pkg/integration/process_test.go
@@ -0,0 +1,144 @@
+package integration
+
+import (
+ "bytes"
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/gnolang/gno/gnovm/pkg/gnoenv"
+ "github.com/gnolang/gno/tm2/pkg/bft/rpc/client"
+ "github.com/gnolang/gno/tm2/pkg/crypto/ed25519"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// Define a flag to indicate whether to run the embedded command
+var runCommand = flag.Bool("run-node-process", false, "execute the embedded command")
+
+func TestMain(m *testing.M) {
+ flag.Parse()
+
+ // Check if the embedded command should be executed
+ if !*runCommand {
+ fmt.Println("Running tests...")
+ os.Exit(m.Run())
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+ defer cancel()
+
+ if err := RunMain(ctx, os.Stdin, os.Stdout, os.Stderr); err != nil {
+ fmt.Fprintln(os.Stderr, err.Error())
+ os.Exit(1)
+ }
+}
+
+// TestGnolandIntegration tests the forking of a Gnoland node.
+func TestNodeProcess(t *testing.T) {
+ t.Parallel()
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
+ defer cancel()
+
+ gnoRootDir := gnoenv.RootDir()
+
+ // Define paths for the build directory and the gnoland binary
+ gnolandDBDir := filepath.Join(t.TempDir(), "db")
+
+ // Prepare a minimal node configuration for testing
+ cfg := TestingMinimalNodeConfig(gnoRootDir)
+
+ var stdio bytes.Buffer
+ defer func() {
+ t.Log("node output:")
+ t.Log(stdio.String())
+ }()
+
+ start := time.Now()
+ node := runTestingNodeProcess(t, ctx, ProcessConfig{
+ Stderr: &stdio, Stdout: &stdio,
+ Node: &ProcessNodeConfig{
+ Verbose: true,
+ ValidatorKey: ed25519.GenPrivKey(),
+ DBDir: gnolandDBDir,
+ RootDir: gnoRootDir,
+ TMConfig: cfg.TMConfig,
+ Genesis: NewMarshalableGenesisDoc(cfg.Genesis),
+ },
+ })
+ t.Logf("time to start the node: %v", time.Since(start).String())
+
+ // Create a new HTTP client to interact with the integration node
+ cli, err := client.NewHTTPClient(node.Address())
+ require.NoError(t, err)
+
+ // Retrieve node info
+ info, err := cli.ABCIInfo()
+ require.NoError(t, err)
+ assert.NotEmpty(t, info.Response.Data)
+
+ // Attempt to stop the node
+ err = node.Stop()
+ require.NoError(t, err)
+
+ // Attempt to stop the node a second time, should not fail
+ err = node.Stop()
+ require.NoError(t, err)
+}
+
+// TestGnolandIntegration tests the forking of a Gnoland node.
+func TestInMemoryNodeProcess(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
+ defer cancel()
+
+ gnoRootDir := gnoenv.RootDir()
+
+ // Define paths for the build directory and the gnoland binary
+ gnolandDBDir := filepath.Join(t.TempDir(), "db")
+
+ // Prepare a minimal node configuration for testing
+ cfg := TestingMinimalNodeConfig(gnoRootDir)
+
+ var stdio bytes.Buffer
+ defer func() {
+ t.Log("node output:")
+ t.Log(stdio.String())
+ }()
+
+ start := time.Now()
+ node, err := RunInMemoryProcess(ctx, ProcessConfig{
+ Stderr: &stdio, Stdout: &stdio,
+ Node: &ProcessNodeConfig{
+ Verbose: true,
+ ValidatorKey: ed25519.GenPrivKey(),
+ DBDir: gnolandDBDir,
+ RootDir: gnoRootDir,
+ TMConfig: cfg.TMConfig,
+ Genesis: NewMarshalableGenesisDoc(cfg.Genesis),
+ },
+ })
+ require.NoError(t, err)
+ t.Logf("time to start the node: %v", time.Since(start).String())
+
+ // Create a new HTTP client to interact with the integration node
+ cli, err := client.NewHTTPClient(node.Address())
+ require.NoError(t, err)
+
+ // Retrieve node info
+ info, err := cli.ABCIInfo()
+ require.NoError(t, err)
+ assert.NotEmpty(t, info.Response.Data)
+
+ // Attempt to stop the node
+ err = node.Stop()
+ require.NoError(t, err)
+
+ // Attempt to stop the node a second time, should not fail
+ err = node.Stop()
+ require.NoError(t, err)
+}
diff --git a/gno.land/pkg/integration/testdata/addpkg.txtar b/gno.land/pkg/integration/testdata/addpkg.txtar
new file mode 100644
index 00000000000..15e8ad222f2
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/addpkg.txtar
@@ -0,0 +1,34 @@
+# test for add package
+
+## start a new node
+gnoland start
+
+## deploy realm
+gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$test1_user_addr/hello -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
+
+## check output
+stdout OK!
+stdout 'GAS WANTED: 100000000'
+stdout 'GAS USED: \d+'
+stdout 'HEIGHT: \d+'
+stdout 'EVENTS: \[\]'
+stdout 'TX HASH: '
+
+## call added realm
+gnokey maketx call -pkgpath gno.land/r/$test1_user_addr/hello -chainid=tendermint_test -func SayHello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast test1
+
+## check output
+stdout '\("hello world!" string\)'
+stdout OK!
+stdout 'GAS WANTED: 2000000'
+stdout 'GAS USED: \d+'
+stdout 'HEIGHT: \d+'
+stdout 'EVENTS: \[\]'
+stdout 'TX HASH: '
+
+-- hello.gno --
+package hello
+
+func SayHello() string {
+ return "hello world!"
+}
diff --git a/gno.land/pkg/integration/testdata/addpkg_domain.txtar b/gno.land/pkg/integration/testdata/addpkg_domain.txtar
new file mode 100644
index 00000000000..25e4fe0d3a3
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/addpkg_domain.txtar
@@ -0,0 +1,15 @@
+gnoland start
+
+# addpkg with anotherdomain.land
+! gnokey maketx addpkg -pkgdir $WORK -pkgpath anotherdomain.land/r/foobar/bar -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
+stdout 'TX HASH:'
+stderr 'invalid package path'
+stderr 'invalid domain: anotherdomain.land/r/foobar/bar'
+
+# addpkg with gno.land
+gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/foobar/bar -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
+stdout 'OK!'
+
+-- bar.gno --
+package bar
+func Render(path string) string { return "hello" }
diff --git a/gno.land/cmd/gnoland/testdata/addpkg_invalid.txtar b/gno.land/pkg/integration/testdata/addpkg_invalid.txtar
similarity index 100%
rename from gno.land/cmd/gnoland/testdata/addpkg_invalid.txtar
rename to gno.land/pkg/integration/testdata/addpkg_invalid.txtar
diff --git a/gno.land/pkg/integration/testdata/addpkg_namespace.txtar b/gno.land/pkg/integration/testdata/addpkg_namespace.txtar
new file mode 100644
index 00000000000..2cfd00acda4
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/addpkg_namespace.txtar
@@ -0,0 +1,90 @@
+loadpkg gno.land/r/demo/users
+loadpkg gno.land/r/sys/users
+
+adduser admin
+adduser gui
+
+patchpkg "g1manfred47kzduec920z88wfr64ylksmdcedlf5" $admin_user_addr # 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 200000 -broadcast -chainid tendermint_test gui
+stdout 'OK!'
+stdout 'false'
+
+# Gui should be able to addpkg on test1 addr
+# gui addpkg -> gno.land/r//mysuperpkg
+gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$test1_user_addr/mysuperpkg -gas-fee 1000000ugnot -gas-wanted 400000 -broadcast -chainid=tendermint_test gui
+stdout 'OK!'
+
+# Gui should be able to addpkg on random name
+# gui addpkg -> gno.land/r/randomname/mysuperpkg
+gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/randomname/mysuperpkg -gas-fee 1000000ugnot -gas-wanted 350000 -broadcast -chainid=tendermint_test gui
+stdout 'OK!'
+
+## When `sys/users` is enabled
+
+# Enable `sys/users`
+# admin call -> sys/users.AdminEnable
+gnokey maketx call -pkgpath gno.land/r/sys/users -func AdminEnable -gas-fee 100000ugnot -gas-wanted 1000000 -broadcast -chainid tendermint_test admin
+stdout 'OK!'
+
+# Check that `sys/users` has been enabled
+# gui call -> sys/users.IsEnable
+gnokey maketx call -pkgpath gno.land/r/sys/users -func IsEnabled -gas-fee 100000ugnot -gas-wanted 200000 -broadcast -chainid tendermint_test gui
+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/$test1_user_addr/one -gas-fee 1000000ugnot -gas-wanted 1000000 -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/$gui_user_addr/one -gas-fee 1000000ugnot -gas-wanted 1000000 -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
+# This is expected to fail at the transaction simulation stage, which is why we set gas-wanted to 1.
+! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guiland/one -gas-fee 1000000ugnot -gas-wanted 1 -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 2500000 -broadcast -chainid=tendermint_test -args $gui_user_addr admin
+stdout 'OK!'
+
+# test gui register namespace
+# gui call -> demo/users.Register
+gnokey maketx call -pkgpath gno.land/r/demo/users -func Register -gas-fee 1000000ugnot -gas-wanted 2500000 -broadcast -chainid=tendermint_test -args $admin_user_addr -args 'guiland' -args 'im gui' gui
+stdout 'OK!'
+
+# Test gui publishing on guiland/one
+# gui addpkg -> gno.land/r/guiland/one
+gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guiland/one -gas-fee 1000000ugnot -gas-wanted 1800000 -broadcast -chainid=tendermint_test gui
+stdout 'OK!'
+
+# Test admin publishing on guiland/two
+# admin addpkg -> gno.land/r/guiland/two
+# This is expected to fail at the transaction simulation stage, which is why we set gas-wanted to 1.
+! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guiland/two -gas-fee 1000000ugnot -gas-wanted 1 -broadcast -chainid=tendermint_test admin
+stderr 'unauthorized user'
+
+-- one.gno --
+package one
+
+func Render(path string) string {
+ return "# Hello One"
+}
diff --git a/gno.land/pkg/integration/testdata/addpkg_outofgas.txtar b/gno.land/pkg/integration/testdata/addpkg_outofgas.txtar
new file mode 100644
index 00000000000..fc536b705c6
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/addpkg_outofgas.txtar
@@ -0,0 +1,50 @@
+# ensure users get proper out of gas errors when they add packages
+
+# start a new node
+gnoland start
+
+# add foo package
+gnokey maketx addpkg -pkgdir $WORK/foo -pkgpath gno.land/r/foo -gas-fee 1000000ugnot -gas-wanted 220000 -broadcast -chainid=tendermint_test test1
+
+
+# add bar package - out of gas at store.GetPackage() with gas 60000
+
+! gnokey maketx addpkg -pkgdir $WORK/bar -pkgpath gno.land/r/bar -gas-fee 1000000ugnot -gas-wanted 60000 -broadcast -chainid=tendermint_test test1
+
+stderr '--= Error =--'
+stderr 'Data: out of gas error'
+stderr '--= /Error =--'
+
+
+
+# out of gas at store.store.GetTypeSafe() with gas 63000
+
+! gnokey maketx addpkg -pkgdir $WORK/bar -pkgpath gno.land/r/bar -gas-fee 1000000ugnot -gas-wanted 63000 -broadcast -chainid=tendermint_test test1
+
+stderr '--= Error =--'
+stderr 'Data: out of gas error'
+stderr '--= /Error =--'
+
+
+-- foo/foo.gno --
+package foo
+
+type Counter int
+
+func Inc(i Counter) Counter{
+ i = i+1
+ return i
+}
+
+-- bar/bar.gno --
+package bar
+
+import "gno.land/r/foo"
+
+type NewCounter foo.Counter
+
+func Add2(i NewCounter) NewCounter{
+ i=i+2
+
+ return i
+}
diff --git a/gno.land/pkg/integration/testdata/adduserfrom.txtar b/gno.land/pkg/integration/testdata/adduserfrom.txtar
index a23849aa604..8bbfaa738fd 100644
--- a/gno.land/pkg/integration/testdata/adduserfrom.txtar
+++ b/gno.land/pkg/integration/testdata/adduserfrom.txtar
@@ -14,21 +14,21 @@ stdout 'g1mtmrdmqfu0aryqfl4aw65n35haw2wdjkh5p4cp'
gnoland start
## check users initial balance
-gnokey query bank/balances/${USER_ADDR_user1}
+gnokey query bank/balances/$user1_user_addr
stdout '10000000ugnot'
gnokey query bank/balances/g18e22n23g462drp4pyszyl6e6mwxkaylthgeeq4
stdout '10000000ugnot'
-gnokey query auth/accounts/${USER_ADDR_user3}
+gnokey query auth/accounts/$user3_user_addr
stdout 'height: 0'
stdout 'data: {'
stdout ' "BaseAccount": {'
stdout ' "address": "g1mtmrdmqfu0aryqfl4aw65n35haw2wdjkh5p4cp",'
stdout ' "coins": "10000000ugnot",'
stdout ' "public_key": null,'
-stdout ' "account_number": "58",'
+stdout ' "account_number": "59",'
stdout ' "sequence": "0"'
stdout ' }'
stdout '}'
-! stderr '.+' # empty
\ No newline at end of file
+! stderr '.+' # empty
diff --git a/gno.land/pkg/integration/testdata/alloc_array.txtar b/gno.land/pkg/integration/testdata/alloc_array.txtar
new file mode 100644
index 00000000000..df9e6539297
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/alloc_array.txtar
@@ -0,0 +1,16 @@
+loadpkg gno.land/r/alloc $WORK
+
+gnoland start
+
+! gnokey maketx call -pkgpath gno.land/r/alloc -func DoAlloc -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
+stderr 'Data: allocation limit exceeded'
+
+-- alloc.gno --
+package alloc
+
+var buffer interface{}
+
+func DoAlloc() {
+ var arr [1_000_000_000_000_000]byte
+ buffer = arr
+}
diff --git a/gno.land/pkg/integration/testdata/alloc_byte_slice.txtar b/gno.land/pkg/integration/testdata/alloc_byte_slice.txtar
new file mode 100644
index 00000000000..99b0b85e97d
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/alloc_byte_slice.txtar
@@ -0,0 +1,16 @@
+loadpkg gno.land/r/alloc $WORK
+
+gnoland start
+
+! gnokey maketx call -pkgpath gno.land/r/alloc -func DoAlloc -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
+stderr 'Data: allocation limit exceeded'
+
+-- alloc.gno --
+package alloc
+
+var buffer []byte
+
+func DoAlloc() {
+ buffer := make([]byte, 1_000_000_000_000)
+ buffer[1] = 'a'
+}
diff --git a/gno.land/pkg/integration/testdata/alloc_slice.txtar b/gno.land/pkg/integration/testdata/alloc_slice.txtar
new file mode 100644
index 00000000000..21a4d28d90d
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/alloc_slice.txtar
@@ -0,0 +1,16 @@
+loadpkg gno.land/r/alloc $WORK
+
+gnoland start
+
+! gnokey maketx call -pkgpath gno.land/r/alloc -func DoAlloc -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
+stderr 'Data: allocation limit exceeded'
+
+-- alloc.gno --
+package alloc
+
+var buffer []int
+
+func DoAlloc() {
+ buffer := make([]int, 1_000_000_000_000)
+ buffer[1] = 1
+}
diff --git a/gno.land/pkg/integration/testdata/append.txtar b/gno.land/pkg/integration/testdata/append.txtar
new file mode 100644
index 00000000000..c5c5272d3be
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/append.txtar
@@ -0,0 +1,131 @@
+loadpkg gno.land/p/demo/ufmt
+
+# start a new node
+gnoland start
+
+gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/append -gas-fee 1000000ugnot -gas-wanted 9000000 -broadcast -chainid=tendermint_test test1
+stdout OK!
+
+# Call Append 1
+gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 300000 -args '1' -broadcast -chainid=tendermint_test test1
+stdout OK!
+
+gnokey maketx call -pkgpath gno.land/r/append -func AppendNil -gas-fee 1000000ugnot -gas-wanted 300000 -broadcast -chainid=tendermint_test test1
+stdout OK!
+
+# Call Append 2
+gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 300000 -args '2' -broadcast -chainid=tendermint_test test1
+stdout OK!
+
+# Call Append 3
+gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 300000 -args '3' -broadcast -chainid=tendermint_test test1
+stdout OK!
+
+# Call render
+gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 1500000 -args '' -broadcast -chainid=tendermint_test test1
+stdout '("1-2-3-" string)'
+stdout OK!
+
+# Call Pop
+gnokey maketx call -pkgpath gno.land/r/append -func Pop -gas-fee 1000000ugnot -gas-wanted 300000 -broadcast -chainid=tendermint_test test1
+stdout OK!
+
+# Call render
+gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 1500000 -args '' -broadcast -chainid=tendermint_test test1
+stdout '("2-3-" string)'
+stdout OK!
+
+# Call Append 42
+gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 300000 -args '42' -broadcast -chainid=tendermint_test test1
+stdout OK!
+
+# Call render
+gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 1500000 -args '' -broadcast -chainid=tendermint_test test1
+stdout '("2-3-42-" string)'
+stdout OK!
+
+gnokey maketx call -pkgpath gno.land/r/append -func CopyAppend -gas-fee 1000000ugnot -gas-wanted 300000 -broadcast -chainid=tendermint_test test1
+stdout OK!
+
+gnokey maketx call -pkgpath gno.land/r/append -func PopB -gas-fee 1000000ugnot -gas-wanted 350000 -broadcast -chainid=tendermint_test test1
+stdout OK!
+
+# Call render
+gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 1500000 -args '' -broadcast -chainid=tendermint_test test1
+stdout '("2-3-42-" string)'
+stdout OK!
+
+gnokey maketx call -pkgpath gno.land/r/append -func AppendMoreAndC -gas-fee 1000000ugnot -gas-wanted 350000 -broadcast -chainid=tendermint_test test1
+stdout OK!
+
+gnokey maketx call -pkgpath gno.land/r/append -func ReassignC -gas-fee 1000000ugnot -gas-wanted 350000 -broadcast -chainid=tendermint_test test1
+stdout OK!
+
+gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 1500000 -args '' -broadcast -chainid=tendermint_test test1
+stdout '("2-3-42-70-100-" string)'
+stdout OK!
+
+gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 1500000 -args 'd' -broadcast -chainid=tendermint_test test1
+stdout '("1-" string)'
+stdout OK!
+
+-- append.gno --
+package append
+
+import (
+ "gno.land/p/demo/ufmt"
+)
+
+type T struct{ i int }
+
+var a, b, d []T
+var c = []T{{i: 100}}
+
+
+func init() {
+ a = make([]T, 0, 1)
+}
+
+func Pop() {
+ a = append(a[:0], a[1:]...)
+}
+
+func Append(i int) {
+ a = append(a, T{i: i})
+}
+
+func CopyAppend() {
+ b = append(a, T{i: 50}, T{i: 60})
+}
+
+func PopB() {
+ b = append(b[:0], b[1:]...)
+}
+
+func AppendMoreAndC() {
+ // Fill to capacity
+ a = append(a, T{i: 70})
+ // Above capacity; make new array
+ a = append(a, c...)
+}
+
+func ReassignC() {
+ c[0] = T{i: 200}
+}
+
+func AppendNil() {
+ d = append(d, a...)
+}
+
+func Render(path string) string {
+ source := a
+ if path == "d" {
+ source = d
+ }
+
+ var s string
+ for i:=0;i myrlm.A: PANIC
+! gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 1 -broadcast -chainid tendermint_test test1
+stderr 'invalid non-origin call'
+
+## 2. MsgCall -> myrlm.B: PASS
+gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 150000 -broadcast -chainid tendermint_test test1
+stdout 'OK!'
+
+## 3. MsgCall -> myrlm.C: PASS
+gnokey maketx call -pkgpath gno.land/r/myrlm -func C -gas-fee 100000ugnot -gas-wanted 1500000 -broadcast -chainid tendermint_test test1
+stdout 'OK!'
+
+## 4. MsgCall -> r/foo.A -> myrlm.A: PANIC
+! gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 1 -broadcast -chainid tendermint_test test1
+stderr 'invalid non-origin call'
+
+## 5. MsgCall -> r/foo.B -> myrlm.B: PASS
+gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 200000 -broadcast -chainid tendermint_test test1
+stdout 'OK!'
+
+## 6. MsgCall -> r/foo.C -> myrlm.C: PANIC
+! gnokey maketx call -pkgpath gno.land/r/foo -func C -gas-fee 100000ugnot -gas-wanted 10000000 -broadcast -chainid tendermint_test test1
+stderr 'invalid non-origin call'
+
+## remove due to update to maketx call can only call realm (case 7,8,9)
+## 7. MsgCall -> p/demo/bar.A: PANIC
+## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1
+## stderr 'invalid non-origin call'
+
+## 8. MsgCall -> p/demo/bar.B: PASS
+## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1
+## stdout 'OK!'
+
+## 9. MsgCall -> p/demo/bar.C: PANIC
+## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func C -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1
+## stderr 'invalid non-origin call'
+
+## 10. MsgRun -> run.main -> myrlm.A: PANIC
+! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5500000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno
+stderr 'invalid non-origin call'
+
+## 11. MsgRun -> run.main -> myrlm.B: PASS
+gnokey maketx run -gas-fee 100000ugnot -gas-wanted 10000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno
+stdout 'OK!'
+
+## 12. MsgRun -> run.main -> myrlm.C: PANIC
+! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5500000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmC.gno
+stderr 'invalid non-origin call'
+
+## 13. MsgRun -> run.main -> foo.A: PANIC
+! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5500000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno
+stderr 'invalid non-origin call'
+
+## 14. MsgRun -> run.main -> foo.B: PASS
+gnokey maketx run -gas-fee 100000ugnot -gas-wanted 10000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno
+stdout 'OK!'
+
+## 15. MsgRun -> run.main -> foo.C: PANIC
+! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5500000 -broadcast -chainid tendermint_test test1 $WORK/run/fooC.gno
+stderr 'invalid non-origin call'
+
+## 16. MsgRun -> run.main -> bar.A: PANIC
+! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5500000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno
+stderr 'invalid non-origin call'
+
+## 17. MsgRun -> run.main -> bar.B: PASS
+gnokey maketx run -gas-fee 100000ugnot -gas-wanted 10000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno
+stdout 'OK!'
+
+## 18. MsgRun -> run.main -> bar.C: PANIC
+! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5500000 -broadcast -chainid tendermint_test test1 $WORK/run/barC.gno
+stderr 'invalid non-origin call'
+
+## remove testcase 19 due to maketx call forced to call a realm
+## 19. MsgCall -> std.AssertOriginCall: pass
+## gnokey maketx call -pkgpath std -func AssertOriginCall -gas-fee 100000ugnot -gas-wanted 10000000 -broadcast -chainid tendermint_test test1
+## stdout 'OK!'
+
+## 20. MsgRun -> std.AssertOriginCall: PANIC
+! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 10000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno
+stderr 'invalid non-origin call'
+
+
+-- r/myrlm/rlm.gno --
+package myrlm
+
+import "std"
+
+func A() {
+ C()
+}
+
+func B() {
+ if false {
+ C()
+ }
+}
+
+func C() {
+ std.AssertOriginCall()
+}
+-- r/foo/foo.gno --
+package foo
+
+import "gno.land/r/myrlm"
+
+func A() {
+ myrlm.A()
+}
+
+func B() {
+ myrlm.B()
+}
+
+func C() {
+ myrlm.C()
+}
+-- p/demo/bar/bar.gno --
+package bar
+
+import "std"
+
+func A() {
+ C()
+}
+
+func B() {
+ if false {
+ C()
+ }
+}
+func C() {
+ std.AssertOriginCall()
+}
+-- run/myrlmA.gno --
+package main
+
+import myrlm "gno.land/r/myrlm"
+
+func main() {
+ myrlm.A()
+}
+-- run/myrlmB.gno --
+package main
+
+import "gno.land/r/myrlm"
+
+func main() {
+ myrlm.B()
+}
+-- run/myrlmC.gno --
+package main
+
+import "gno.land/r/myrlm"
+
+func main() {
+ myrlm.C()
+}
+-- run/fooA.gno --
+package main
+
+import "gno.land/r/foo"
+
+func main() {
+ foo.A()
+}
+-- run/fooB.gno --
+package main
+
+import "gno.land/r/foo"
+
+func main() {
+ foo.B()
+}
+-- run/fooC.gno --
+package main
+
+import "gno.land/r/foo"
+
+func main() {
+ foo.C()
+}
+-- run/barA.gno --
+package main
+
+import "gno.land/p/demo/bar"
+
+func main() {
+ bar.A()
+}
+-- run/barB.gno --
+package main
+
+import "gno.land/p/demo/bar"
+
+func main() {
+ bar.B()
+}
+-- run/barC.gno --
+package main
+
+import "gno.land/p/demo/bar"
+
+func main() {
+ bar.C()
+}
+-- run/baz.gno --
+package main
+
+import "std"
+
+func main() {
+ std.AssertOriginCall()
+}
diff --git a/gno.land/cmd/gnoland/testdata/event_callback.txtar b/gno.land/pkg/integration/testdata/event_callback.txtar
similarity index 100%
rename from gno.land/cmd/gnoland/testdata/event_callback.txtar
rename to gno.land/pkg/integration/testdata/event_callback.txtar
diff --git a/gno.land/cmd/gnoland/testdata/event_defer_callback_loop.txtar b/gno.land/pkg/integration/testdata/event_defer_callback_loop.txtar
similarity index 100%
rename from gno.land/cmd/gnoland/testdata/event_defer_callback_loop.txtar
rename to gno.land/pkg/integration/testdata/event_defer_callback_loop.txtar
diff --git a/gno.land/cmd/gnoland/testdata/event_for_statement.txtar b/gno.land/pkg/integration/testdata/event_for_statement.txtar
similarity index 100%
rename from gno.land/cmd/gnoland/testdata/event_for_statement.txtar
rename to gno.land/pkg/integration/testdata/event_for_statement.txtar
diff --git a/gno.land/pkg/integration/testdata/event_multi_msg.txtar b/gno.land/pkg/integration/testdata/event_multi_msg.txtar
new file mode 100644
index 00000000000..3c5667b73b0
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/event_multi_msg.txtar
@@ -0,0 +1,53 @@
+# load the package from $WORK directory
+loadpkg gno.land/r/demo/simple_event $WORK/event
+
+# add a random user
+adduserfrom user1 'success myself purchase tray reject demise scene little legend someone lunar hope media goat regular test area smart save flee surround attack rapid smoke'
+stdout 'g1c0j899h88nwyvnzvh5jagpq6fkkyuj76nld6t0'
+
+# start a new node
+gnoland start
+
+## account should be available since it has an initial balance
+gnokey query auth/accounts/g1c0j899h88nwyvnzvh5jagpq6fkkyuj76nld6t0
+stdout 'height: 0'
+stdout 'data: {'
+stdout ' "BaseAccount": {'
+stdout ' "address": "'${user1_user_addr}'",'
+stdout ' "coins": "[0-9]*ugnot",' # dynamic
+stdout ' "public_key": null,'
+stdout ' "account_number": "'${user1_account_num}'",'
+stdout ' "sequence": "'${user1_account_seq}'"'
+stdout ' }'
+stdout '}'
+! stderr '.+' # empty
+
+
+## sign
+gnokey sign -tx-path $WORK/multi/multi_msg.tx -chainid=tendermint_test -account-number $user1_account_num -account-sequence $user1_account_seq user1
+stdout 'Tx successfully signed and saved to '
+
+## broadcast
+gnokey broadcast $WORK/multi/multi_msg.tx -quiet=false
+
+stdout OK!
+stdout 'GAS WANTED: 2000000'
+stdout 'GAS USED: [0-9]+'
+stdout 'HEIGHT: [0-9]+'
+stdout 'EVENTS: \[{\"type\":\"TAG\",\"attrs\":\[{\"key\":\"KEY\",\"value\":\"value11\"}\],\"pkg_path\":\"gno.land\/r\/demo\/simple_event\",\"func\":\"Event\"},{\"type\":\"TAG\",\"attrs\":\[{\"key\":\"KEY\",\"value\":\"value22\"}\],\"pkg_path\":\"gno.land\/r\/demo\/simple_event\",\"func\":\"Event\"}\]'
+
+
+
+-- event/simple_event.gno --
+package simple_event
+
+import (
+ "std"
+)
+
+func Event(value string) {
+ std.Emit("TAG", "KEY", value)
+}
+
+-- multi/multi_msg.tx --
+{"msg":[{"@type":"/vm.m_call","caller":"g1c0j899h88nwyvnzvh5jagpq6fkkyuj76nld6t0","send":"","pkg_path":"gno.land/r/demo/simple_event","func":"Event","args":["value11"]},{"@type":"/vm.m_call","caller":"g1c0j899h88nwyvnzvh5jagpq6fkkyuj76nld6t0","send":"","pkg_path":"gno.land/r/demo/simple_event","func":"Event","args":["value22"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":null,"memo":""}
diff --git a/gno.land/cmd/gnoland/testdata/event_normal.txtar b/gno.land/pkg/integration/testdata/event_normal.txtar
similarity index 100%
rename from gno.land/cmd/gnoland/testdata/event_normal.txtar
rename to gno.land/pkg/integration/testdata/event_normal.txtar
diff --git a/gno.land/cmd/gnoland/testdata/float_arg.txtar b/gno.land/pkg/integration/testdata/float_arg.txtar
similarity index 100%
rename from gno.land/cmd/gnoland/testdata/float_arg.txtar
rename to gno.land/pkg/integration/testdata/float_arg.txtar
diff --git a/gno.land/pkg/integration/testdata/genesis_params.txtar b/gno.land/pkg/integration/testdata/genesis_params.txtar
new file mode 100644
index 00000000000..d09ededf78a
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/genesis_params.txtar
@@ -0,0 +1,28 @@
+# Test for #3003, #2911.
+
+gnoland start
+
+# Query and validate official parameters.
+# These parameters should ideally be tested in a txtar format to ensure that a
+# default initialization of "gnoland" provides the expected default values.
+
+# Verify the default chain domain parameter for Gno.land
+gnokey query params/vm/gno.land/r/sys/params.vm.chain_domain.string
+stdout 'data: "gno.land"$'
+
+# Test custom parameters to confirm they return the expected values and types.
+
+gnokey query params/vm/gno.land/r/sys/params.test.foo.string
+stdout 'data: "bar"$'
+
+gnokey query params/vm/gno.land/r/sys/params.test.foo.int64
+stdout 'data: "-1337"'
+
+gnokey query params/vm/gno.land/r/sys/params.test.foo.uint64
+stdout 'data: "42"'
+
+gnokey query params/vm/gno.land/r/sys/params.test.foo.bool
+stdout 'data: true'
+
+# TODO: Consider adding a test case for a byte array parameter
+
diff --git a/gno.land/pkg/integration/testdata/ghverify.txtar b/gno.land/pkg/integration/testdata/ghverify.txtar
new file mode 100644
index 00000000000..0f2d21f6bd5
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/ghverify.txtar
@@ -0,0 +1,40 @@
+loadpkg gno.land/r/gnoland/ghverify
+
+# start the node
+gnoland start
+
+# make a verification request
+gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func RequestVerification -args 'deelawn' -gas-fee 1000000ugnot -gas-wanted 3500000 -broadcast -chainid=tendermint_test test1
+stdout OK!
+
+# request tasks to complete (this is done by the agent)
+gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GnorkleEntrypoint -args 'request' -gas-fee 1000000ugnot -gas-wanted 6000000 -broadcast -chainid=tendermint_test test1
+stdout '\("\[\{\\"id\\":\\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\\",\\"type\\":\\"0\\",\\"value_type\\":\\"string\\",\\"tasks\\":\[\{\\"gno_address\\":\\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\\",\\"github_handle\\":\\"deelawn\\"\}\]\}\]" string\)'
+
+# a verification request was made but there should be no verified address
+gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetHandleByAddress -args 'g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5' -gas-fee 1000000ugnot -gas-wanted 700000 -broadcast -chainid=tendermint_test test1
+stdout ""
+
+# a verification request was made but there should be no verified handle
+gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetAddressByHandle -args 'deelawn' -gas-fee 1000000ugnot -gas-wanted 700000 -broadcast -chainid=tendermint_test test1
+stdout ""
+
+# fail on ingestion with a bad task ID
+# This is expected to fail at the transaction simulation stage, which is why we set gas-wanted to 1.
+! gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GnorkleEntrypoint -args 'ingest,a' -gas-fee 1000000ugnot -gas-wanted 1 -broadcast -chainid=tendermint_test test1
+stderr 'invalid ingest id: a'
+
+# the agent publishes their response to the task and the verification is complete
+gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GnorkleEntrypoint -args 'ingest,g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5,OK' -gas-fee 1000000ugnot -gas-wanted 6000000 -broadcast -chainid=tendermint_test test1
+stdout OK!
+
+# get verified github handle by gno address
+gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetHandleByAddress -args 'g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5' -gas-fee 1000000ugnot -gas-wanted 700000 -broadcast -chainid=tendermint_test test1
+stdout "deelawn"
+
+# get verified gno address by github handle
+gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetAddressByHandle -args 'deelawn' -gas-fee 1000000ugnot -gas-wanted 700000 -broadcast -chainid=tendermint_test test1
+stdout "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"
+
+gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func Render -args '' -gas-fee 1000000ugnot -gas-wanted 700000 -broadcast -chainid=tendermint_test test1
+stdout '\("\{\\"deelawn\\": \\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\\"\}" string\)'
diff --git a/gno.land/pkg/integration/testdata/gnokey.txtar b/gno.land/pkg/integration/testdata/gnokey.txtar
index 123a0ce291c..3268782b1ca 100644
--- a/gno.land/pkg/integration/testdata/gnokey.txtar
+++ b/gno.land/pkg/integration/testdata/gnokey.txtar
@@ -1,19 +1,22 @@
# test basic gnokey integrations commands
# golden files have been generated using UPDATE_SCRIPTS=true
+# add a random user
+adduser user1
+
# start gnoland
gnoland start
## test1 account should be available on default
-gnokey query auth/accounts/${USER_ADDR_test1}
+gnokey query auth/accounts/$user1_user_addr
stdout 'height: 0'
stdout 'data: {'
stdout ' "BaseAccount": {'
-stdout ' "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",'
+stdout ' "address": "'${user1_user_addr}'",'
stdout ' "coins": "[0-9]*ugnot",' # dynamic
stdout ' "public_key": null,'
-stdout ' "account_number": "0",'
-stdout ' "sequence": "0"'
+stdout ' "account_number": "'${user1_account_num}'",'
+stdout ' "sequence": "'${user1_account_seq}'"'
stdout ' }'
stdout '}'
! stderr '.+' # empty
diff --git a/gno.land/cmd/gnoland/testdata/gnokey_simulate.txtar b/gno.land/pkg/integration/testdata/gnokey_simulate.txtar
similarity index 85%
rename from gno.land/cmd/gnoland/testdata/gnokey_simulate.txtar
rename to gno.land/pkg/integration/testdata/gnokey_simulate.txtar
index 8db2c7302fc..31b2249f8bb 100644
--- a/gno.land/cmd/gnoland/testdata/gnokey_simulate.txtar
+++ b/gno.land/pkg/integration/testdata/gnokey_simulate.txtar
@@ -6,42 +6,42 @@ loadpkg gno.land/r/hello $WORK/hello
gnoland start
# Initial state: assert that sequence == 0.
-gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "0"'
+gnokey query auth/accounts/$test1_user_addr
+stdout '"sequence": "1"'
# attempt adding the "test" package.
# the package has a syntax error; simulation should catch this ahead of time and prevent the tx.
# -simulate test
! gnokey maketx addpkg -pkgdir $WORK/test -pkgpath gno.land/r/test -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate test test1
-gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "0"'
+gnokey query auth/accounts/$test1_user_addr
+stdout '"sequence": "1"'
# -simulate only
! gnokey maketx addpkg -pkgdir $WORK/test -pkgpath gno.land/r/test -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate only test1
-gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "0"'
+gnokey query auth/accounts/$test1_user_addr
+stdout '"sequence": "1"'
# -simulate skip
! gnokey maketx addpkg -pkgdir $WORK/test -pkgpath gno.land/r/test -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate skip test1
-gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "1"'
+gnokey query auth/accounts/$test1_user_addr
+stdout '"sequence": "2"'
# attempt calling hello.SetName correctly.
# -simulate test and skip should do it successfully, -simulate only should not.
# -simulate test
gnokey maketx call -pkgpath gno.land/r/hello -func SetName -args John -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate test test1
-gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "2"'
+gnokey query auth/accounts/$test1_user_addr
+stdout '"sequence": "3"'
gnokey query vm/qeval --data "gno.land/r/hello.Hello()"
stdout 'Hello, John!'
# -simulate only
gnokey maketx call -pkgpath gno.land/r/hello -func SetName -args Paul -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate only test1
-gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "2"'
+gnokey query auth/accounts/$test1_user_addr
+stdout '"sequence": "3"'
gnokey query vm/qeval --data "gno.land/r/hello.Hello()"
stdout 'Hello, John!'
# -simulate skip
gnokey maketx call -pkgpath gno.land/r/hello -func SetName -args George -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate skip test1
-gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "3"'
+gnokey query auth/accounts/$test1_user_addr
+stdout '"sequence": "4"'
gnokey query vm/qeval --data "gno.land/r/hello.Hello()"
stdout 'Hello, George!'
@@ -50,20 +50,20 @@ stdout 'Hello, George!'
# none should change the name (ie. panic rollbacks).
# -simulate test
! gnokey maketx call -pkgpath gno.land/r/hello -func Grumpy -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate test test1
-gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "3"'
+gnokey query auth/accounts/$test1_user_addr
+stdout '"sequence": "4"'
gnokey query vm/qeval --data "gno.land/r/hello.Hello()"
stdout 'Hello, George!'
# -simulate only
! gnokey maketx call -pkgpath gno.land/r/hello -func Grumpy -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate only test1
-gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "3"'
+gnokey query auth/accounts/$test1_user_addr
+stdout '"sequence": "4"'
gnokey query vm/qeval --data "gno.land/r/hello.Hello()"
stdout 'Hello, George!'
# -simulate skip
! gnokey maketx call -pkgpath gno.land/r/hello -func Grumpy -gas-wanted 2000000 -gas-fee 1000000ugnot -broadcast -chainid tendermint_test -simulate skip test1
-gnokey query auth/accounts/$USER_ADDR_test1
-stdout '"sequence": "4"'
+gnokey query auth/accounts/$test1_user_addr
+stdout '"sequence": "5"'
gnokey query vm/qeval --data "gno.land/r/hello.Hello()"
stdout 'Hello, George!'
diff --git a/gno.land/pkg/integration/testdata/gnoland.txtar b/gno.land/pkg/integration/testdata/gnoland.txtar
index c675e7578b6..83c8fe9c9a5 100644
--- a/gno.land/pkg/integration/testdata/gnoland.txtar
+++ b/gno.land/pkg/integration/testdata/gnoland.txtar
@@ -28,7 +28,7 @@ cmp stderr gnoland-already-stop.stderr.golden
-- gnoland-no-arguments.stdout.golden --
-- gnoland-no-arguments.stderr.golden --
-"gnoland" error: syntax: gnoland [start|stop]
+"gnoland" error: no command provided
-- gnoland-start.stdout.golden --
node started successfully
-- gnoland-start.stderr.golden --
diff --git a/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar b/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar
new file mode 100644
index 00000000000..838db121442
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar
@@ -0,0 +1,42 @@
+# This test ensures that the "full security with airgap" commands, on gnoweb's
+# help page, work as intended.
+
+# load the package from $WORK directory
+loadpkg gno.land/r/demo/echo
+
+# add a random user
+adduserfrom user1 'lamp any denial pulse used shoot gap error denial mansion hurry foot solution grab winner congress drastic cat bamboo chicken color digital coffee unknown'
+stdout 'g1meuazsmy8ztaz2xpuyraqq4axy6s00ycl07zva'
+
+# start the node
+gnoland start
+
+# Query account
+gnokey query auth/accounts/g1meuazsmy8ztaz2xpuyraqq4axy6s00ycl07zva
+stdout 'height: 0'
+stdout 'data: {'
+stdout ' "BaseAccount": {'
+stdout ' "address": "g1meuazsmy8ztaz2xpuyraqq4axy6s00ycl07zva",'
+stdout ' "coins": "[0-9]*ugnot",' # dynamic
+stdout ' "public_key": null,'
+stdout ' "account_number": "57",'
+stdout ' "sequence": "0"'
+stdout ' }'
+stdout '}'
+! stderr '.+' # empty
+
+# Create transaction
+gnokey maketx call -pkgpath "gno.land/r/demo/echo" -func "Render" -gas-fee 1000000ugnot -gas-wanted 2000000 -send "" -args "HELLO" user1
+cp stdout call.tx
+
+# Sign
+gnokey sign -tx-path $WORK/call.tx -chainid "tendermint_test" -account-number 57 -account-sequence 0 user1
+cmpenv stdout sign.stdout.golden
+
+gnokey broadcast $WORK/call.tx
+stdout '("HELLO" string)'
+stdout 'GAS WANTED: 2000000'
+
+-- sign.stdout.golden --
+
+Tx successfully signed and saved to $WORK/call.tx
diff --git a/gno.land/pkg/integration/testdata/grc20_invalid_address.txtar b/gno.land/pkg/integration/testdata/grc20_invalid_address.txtar
new file mode 100644
index 00000000000..0068384903e
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/grc20_invalid_address.txtar
@@ -0,0 +1,13 @@
+# Test for https://github.com/gnolang/gno/pull/1799
+loadpkg gno.land/r/demo/foo20
+
+gnoland start
+
+# execute Faucet
+gnokey maketx call -pkgpath gno.land/r/demo/foo20 -func Faucet -gas-fee 10000000ugnot -gas-wanted 40000000 -broadcast -chainid=tendermint_test test1
+stdout 'OK!'
+
+# execute Transfer for invalid address
+# This is expected to fail at the transaction simulation stage, which is why we set gas-wanted to 1.
+! gnokey maketx call -pkgpath gno.land/r/demo/foo20 -func Transfer -args g1ubwj0apf60hd90txhnh855fkac34rxlsvua0aa -args 1 -gas-fee 1000000ugnot -gas-wanted 1 -broadcast -chainid=tendermint_test test1
+stderr '"gnokey" error: --= Error =--\nData: invalid address'
diff --git a/gno.land/cmd/gnoland/testdata/grc20_registry.txtar b/gno.land/pkg/integration/testdata/grc20_registry.txtar
similarity index 82%
rename from gno.land/cmd/gnoland/testdata/grc20_registry.txtar
rename to gno.land/pkg/integration/testdata/grc20_registry.txtar
index 20e78f7ba6e..4377e10a575 100644
--- a/gno.land/cmd/gnoland/testdata/grc20_registry.txtar
+++ b/gno.land/pkg/integration/testdata/grc20_registry.txtar
@@ -6,15 +6,15 @@ loadpkg gno.land/r/registry $WORK/registry
gnoland start
# we call Transfer with foo20, before it's registered
-gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
+gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 1500000 -broadcast -chainid=tendermint_test test1
stdout 'not found'
# add foo20, and foo20wrapper
-gnokey maketx addpkg -pkgdir $WORK/foo20 -pkgpath gno.land/r/foo20 -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
-gnokey maketx addpkg -pkgdir $WORK/foo20wrapper -pkgpath gno.land/r/foo20wrapper -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
+gnokey maketx addpkg -pkgdir $WORK/foo20 -pkgpath gno.land/r/foo20 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+gnokey maketx addpkg -pkgdir $WORK/foo20wrapper -pkgpath gno.land/r/foo20wrapper -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
# we call Transfer with foo20, after it's registered
-gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
+gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 8000000 -broadcast -chainid=tendermint_test test1
stdout 'same address, success!'
-- registry/registry.gno --
@@ -49,7 +49,7 @@ import "gno.land/r/registry"
import "gno.land/r/foo20"
func init() {
- registry.Register("foo20", foo20.Transfer)
+ registry.Register("foo20", foo20.Transfer)
}
-- foo20/foo20.gno --
diff --git a/gno.land/pkg/integration/testdata/grc721_emit.txtar b/gno.land/pkg/integration/testdata/grc721_emit.txtar
new file mode 100644
index 00000000000..45101b74634
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/grc721_emit.txtar
@@ -0,0 +1,95 @@
+# Test for https://github.com/gnolang/gno/pull/3102
+loadpkg gno.land/p/demo/grc/grc721
+loadpkg gno.land/r/demo/users
+loadpkg gno.land/r/foo721 $WORK/foo721
+
+gnoland start
+
+# Mint
+gnokey maketx call -pkgpath gno.land/r/foo721 -func Mint -args g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -args 1 -gas-fee 1000000ugnot -gas-wanted 35000000 -broadcast -chainid=tendermint_test test1
+stdout '\[{\"type\":\"Mint\",\"attrs\":\[{\"key\":\"to\",\"value\":\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},{\"key\":\"tokenId\",\"value\":\"1\"}],\"pkg_path\":\"gno.land\/r\/foo721\",\"func\":\"mint\"}\]'
+
+# Approve
+gnokey maketx call -pkgpath gno.land/r/foo721 -func Approve -args g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj -args 1 -gas-fee 1000000ugnot -gas-wanted 35000000 -broadcast -chainid=tendermint_test test1
+stdout '\[{\"type\":\"Approval\",\"attrs\":\[{\"key\":\"owner\",\"value\":\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},{\"key\":\"to\",\"value\":\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"},{\"key\":\"tokenId\",\"value\":\"1\"}],\"pkg_path\":\"gno.land\/r\/foo721\",\"func\":\"Approve\"}\]'
+
+# SetApprovalForAll
+gnokey maketx call -pkgpath gno.land/r/foo721 -func SetApprovalForAll -args g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj -args false -gas-fee 1000000ugnot -gas-wanted 35000000 -broadcast -chainid=tendermint_test test1
+stdout '\[{\"type\":\"ApprovalForAll\",\"attrs\":\[{\"key\":\"owner\",\"value\":\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},{\"key\":\"to\",\"value\":\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"},{\"key\":\"approved\",\"value\":\"false\"}],\"pkg_path\":\"gno\.land/r/foo721\",\"func\":\"setApprovalForAll\"}\]'
+
+# TransferFrom
+gnokey maketx call -pkgpath gno.land/r/foo721 -func TransferFrom -args g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -args g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj -args 1 -gas-fee 1000000ugnot -gas-wanted 35000000 -broadcast -chainid=tendermint_test test1
+stdout '\[{\"type\":\"Transfer\",\"attrs\":\[{\"key\":\"from\",\"value\":\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},{\"key\":\"to\",\"value\":\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"},{\"key\":\"tokenId\",\"value\":\"1\"}],\"pkg_path\":\"gno\.land/r/foo721\",\"func\":\"transfer\"}\]'
+
+# Burn
+gnokey maketx call -pkgpath gno.land/r/foo721 -func Burn -args 1 -gas-fee 1000000ugnot -gas-wanted 35000000 -broadcast -chainid=tendermint_test test1
+stdout '\[{\"type\":\"Burn\",\"attrs\":\[{\"key\":\"from\",\"value\":\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"},{\"key\":\"tokenId\",\"value\":\"1\"}],\"pkg_path\":\"gno\.land/r/foo721\",\"func\":\"Burn\"}\]'
+
+
+-- foo721/foo721.gno --
+package foo721
+
+import (
+ "std"
+
+ "gno.land/p/demo/grc/grc721"
+ "gno.land/r/demo/users"
+
+ pusers "gno.land/p/demo/users"
+)
+
+var (
+ admin std.Address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"
+ foo = grc721.NewBasicNFT("FooNFT", "FNFT")
+)
+
+// Setters
+
+func Approve(user pusers.AddressOrName, tid grc721.TokenID) {
+ err := foo.Approve(users.Resolve(user), tid)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func SetApprovalForAll(user pusers.AddressOrName, approved bool) {
+ err := foo.SetApprovalForAll(users.Resolve(user), approved)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func TransferFrom(from, to pusers.AddressOrName, tid grc721.TokenID) {
+ err := foo.TransferFrom(users.Resolve(from), users.Resolve(to), tid)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// Admin
+
+func Mint(to pusers.AddressOrName, tid grc721.TokenID) {
+ caller := std.PrevRealm().Addr()
+ assertIsAdmin(caller)
+ err := foo.Mint(users.Resolve(to), tid)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func Burn(tid grc721.TokenID) {
+ caller := std.PrevRealm().Addr()
+ assertIsAdmin(caller)
+ err := foo.Burn(tid)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// Util
+
+func assertIsAdmin(address std.Address) {
+ if address != admin {
+ panic("restricted access")
+ }
+}
diff --git a/gno.land/cmd/gnoland/testdata/initctx.txtar b/gno.land/pkg/integration/testdata/initctx.txtar
similarity index 100%
rename from gno.land/cmd/gnoland/testdata/initctx.txtar
rename to gno.land/pkg/integration/testdata/initctx.txtar
diff --git a/gno.land/cmd/gnoland/testdata/issue_1167.txtar b/gno.land/pkg/integration/testdata/issue_1167.txtar
similarity index 94%
rename from gno.land/cmd/gnoland/testdata/issue_1167.txtar
rename to gno.land/pkg/integration/testdata/issue_1167.txtar
index c43f7a45bd5..7e33d61e9cd 100644
--- a/gno.land/cmd/gnoland/testdata/issue_1167.txtar
+++ b/gno.land/pkg/integration/testdata/issue_1167.txtar
@@ -4,11 +4,11 @@ loadpkg gno.land/p/demo/avl
gnoland start
# add contract
-gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/demo/xx -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
+gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/demo/xx -gas-fee 1000000ugnot -gas-wanted 8000000 -broadcast -chainid=tendermint_test test1
stdout OK!
# execute New
-gnokey maketx call -pkgpath gno.land/r/demo/xx -func New -args X -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
+gnokey maketx call -pkgpath gno.land/r/demo/xx -func New -args X -gas-fee 1000000ugnot -gas-wanted 700000 -broadcast -chainid=tendermint_test test1
stdout OK!
# execute Delta for the first time
diff --git a/gno.land/pkg/integration/testdata/issue_1543.txtar b/gno.land/pkg/integration/testdata/issue_1543.txtar
new file mode 100644
index 00000000000..388f126fcda
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/issue_1543.txtar
@@ -0,0 +1,41 @@
+# test issue
+
+loadpkg gno.land/r/demo/realm $WORK
+
+# start a new node
+gnoland start
+
+
+gnokey maketx call -pkgpath gno.land/r/demo/realm --func Fill --args 0 --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast -chainid=tendermint_test test1
+gnokey maketx call -pkgpath gno.land/r/demo/realm --func UnFill --args 0 --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast -chainid=tendermint_test test1
+gnokey maketx call -pkgpath gno.land/r/demo/realm --func Fill --args 0 --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast -chainid=tendermint_test test1
+
+
+-- realm.gno --
+package main
+
+type A struct {
+ A string
+}
+type B struct {
+ A *A
+ B string
+}
+
+var (
+ a = &A{A: "here"}
+ b [2]*B
+)
+
+func Fill(i int) {
+ c := B{
+ A: a,
+ B: "",
+ }
+ b[i] = &c
+}
+
+func UnFill(i int) {
+ b[i] = nil
+}
+
diff --git a/gno.land/cmd/gnoland/testdata/issue_1588.txtar b/gno.land/pkg/integration/testdata/issue_1588.txtar
similarity index 100%
rename from gno.land/cmd/gnoland/testdata/issue_1588.txtar
rename to gno.land/pkg/integration/testdata/issue_1588.txtar
diff --git a/gno.land/cmd/gnoland/testdata/issue_1786.txtar b/gno.land/pkg/integration/testdata/issue_1786.txtar
similarity index 84%
rename from gno.land/cmd/gnoland/testdata/issue_1786.txtar
rename to gno.land/pkg/integration/testdata/issue_1786.txtar
index 7c92e81dfb6..1cbaf2c6643 100644
--- a/gno.land/cmd/gnoland/testdata/issue_1786.txtar
+++ b/gno.land/pkg/integration/testdata/issue_1786.txtar
@@ -5,24 +5,24 @@ loadpkg gno.land/r/demo/wugnot
gnoland start
# add contract
-gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/demo/proxywugnot -gas-fee 1000000ugnot -gas-wanted 6000000 -broadcast -chainid=tendermint_test test1
+gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/demo/proxywugnot -gas-fee 1000000ugnot -gas-wanted 16000000 -broadcast -chainid=tendermint_test test1
stdout OK!
# approve wugnot to `proxywugnot ≈ g1fndyg0we60rdfchyy5dwxzkfmhl5u34j932rg3`
-gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Approve -args "g1fndyg0we60rdfchyy5dwxzkfmhl5u34j932rg3" -args 10000 -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
+gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Approve -args "g1fndyg0we60rdfchyy5dwxzkfmhl5u34j932rg3" -args 10000 -gas-fee 1000000ugnot -gas-wanted 40000000 -broadcast -chainid=tendermint_test test1
stdout OK!
# send 10000ugnot to `proxywugnot` to wrap it
-gnokey maketx call -pkgpath gno.land/r/demo/proxywugnot --send "10000ugnot" -func ProxyWrap -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
+gnokey maketx call -pkgpath gno.land/r/demo/proxywugnot --send "10000ugnot" -func ProxyWrap -gas-fee 1000000ugnot -gas-wanted 40000000 -broadcast -chainid=tendermint_test test1
stdout OK!
# check user's wugnot balance
-gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func BalanceOf -args "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
+gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func BalanceOf -args "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" -gas-fee 1000000ugnot -gas-wanted 40000000 -broadcast -chainid=tendermint_test test1
stdout OK!
stdout '10000 uint64'
# unwrap 500 wugnot
-gnokey maketx call -pkgpath gno.land/r/demo/proxywugnot -func ProxyUnwrap -args 500 -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
+gnokey maketx call -pkgpath gno.land/r/demo/proxywugnot -func ProxyUnwrap -args 500 -gas-fee 1000000ugnot -gas-wanted 40000000 -broadcast -chainid=tendermint_test test1
# XXX without patching anything it will panic
# panic msg: insufficient coins error
diff --git a/gno.land/pkg/integration/testdata/issue_2266.txtar b/gno.land/pkg/integration/testdata/issue_2266.txtar
new file mode 100644
index 00000000000..046f57802e3
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/issue_2266.txtar
@@ -0,0 +1,42 @@
+# test issue
+
+loadpkg gno.land/r/demo/realm $WORK
+
+# start a new node
+gnoland start
+
+
+gnokey maketx call -pkgpath gno.land/r/demo/realm --func Fill --args 0 --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast -chainid=tendermint_test test1
+gnokey maketx call -pkgpath gno.land/r/demo/realm --func Fill --args 1 --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast -chainid=tendermint_test test1
+gnokey maketx call -pkgpath gno.land/r/demo/realm --func UnFill --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast -chainid=tendermint_test test1
+
+
+-- realm.gno --
+package main
+
+type A struct {
+ A string
+}
+type B struct {
+ A *A
+ B string
+}
+
+var (
+ a = &A{A: "here"}
+ b [2]*B
+)
+
+func Fill(i int) {
+ c := B{
+ A: a,
+ B: "",
+ }
+ b[i] = &c
+}
+
+func UnFill() {
+ b[0] = nil
+ b[1] = nil
+}
+
diff --git a/gno.land/cmd/gnoland/testdata/issue_2283.txtar b/gno.land/pkg/integration/testdata/issue_2283.txtar
similarity index 100%
rename from gno.land/cmd/gnoland/testdata/issue_2283.txtar
rename to gno.land/pkg/integration/testdata/issue_2283.txtar
diff --git a/gno.land/cmd/gnoland/testdata/issue_2283_cacheTypes.txtar b/gno.land/pkg/integration/testdata/issue_2283_cacheTypes.txtar
similarity index 99%
rename from gno.land/cmd/gnoland/testdata/issue_2283_cacheTypes.txtar
rename to gno.land/pkg/integration/testdata/issue_2283_cacheTypes.txtar
index 38b0c8fe865..95bd48c0144 100644
--- a/gno.land/cmd/gnoland/testdata/issue_2283_cacheTypes.txtar
+++ b/gno.land/pkg/integration/testdata/issue_2283_cacheTypes.txtar
@@ -101,4 +101,3 @@ import (
func Call(s string) {
base64.StdEncoding.DecodeString("hey")
}
-
diff --git a/gno.land/cmd/gnoland/testdata/issue_gnochess_97.txtar b/gno.land/pkg/integration/testdata/issue_gnochess_97.txtar
similarity index 100%
rename from gno.land/cmd/gnoland/testdata/issue_gnochess_97.txtar
rename to gno.land/pkg/integration/testdata/issue_gnochess_97.txtar
diff --git a/gno.land/pkg/integration/testdata/loadpkg_example.txtar b/gno.land/pkg/integration/testdata/loadpkg_example.txtar
index d0c95331ff5..f7be500f3b6 100644
--- a/gno.land/pkg/integration/testdata/loadpkg_example.txtar
+++ b/gno.land/pkg/integration/testdata/loadpkg_example.txtar
@@ -4,11 +4,11 @@ loadpkg gno.land/p/demo/ufmt
## start a new node
gnoland start
-gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/importtest -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1
+gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/importtest -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
stdout OK!
## execute Render
-gnokey maketx call -pkgpath gno.land/r/importtest -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1
+gnokey maketx call -pkgpath gno.land/r/importtest -func Render -gas-fee 1000000ugnot -gas-wanted 10000000 -args '' -broadcast -chainid=tendermint_test test1
stdout '("92054" string)'
stdout OK!
@@ -25,4 +25,3 @@ import (
func Render(_ string) string {
return ufmt.Sprintf("%d", 92054)
}
-
diff --git a/gno.land/cmd/gnoland/testdata/maketx_call_pure.txtar b/gno.land/pkg/integration/testdata/maketx_call_pure.txtar
similarity index 100%
rename from gno.land/cmd/gnoland/testdata/maketx_call_pure.txtar
rename to gno.land/pkg/integration/testdata/maketx_call_pure.txtar
diff --git a/gno.land/cmd/gnoland/testdata/map_delete.txtar b/gno.land/pkg/integration/testdata/map_delete.txtar
similarity index 100%
rename from gno.land/cmd/gnoland/testdata/map_delete.txtar
rename to gno.land/pkg/integration/testdata/map_delete.txtar
diff --git a/gno.land/cmd/gnoland/testdata/map_storage.txtar b/gno.land/pkg/integration/testdata/map_storage.txtar
similarity index 100%
rename from gno.land/cmd/gnoland/testdata/map_storage.txtar
rename to gno.land/pkg/integration/testdata/map_storage.txtar
diff --git a/gno.land/cmd/gnoland/testdata/panic.txtar b/gno.land/pkg/integration/testdata/panic.txtar
similarity index 100%
rename from gno.land/cmd/gnoland/testdata/panic.txtar
rename to gno.land/pkg/integration/testdata/panic.txtar
diff --git a/gno.land/pkg/integration/testdata/params.txtar b/gno.land/pkg/integration/testdata/params.txtar
new file mode 100644
index 00000000000..30363aa6369
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/params.txtar
@@ -0,0 +1,65 @@
+# test for https://github.com/gnolang/gno/pull/2920
+
+gnoland start
+
+# query before adding the package
+gnokey query params/vm/gno.land/r/sys/setter.foo.string
+stdout 'data: $'
+gnokey query params/vm/gno.land/r/sys/setter.bar.bool
+stdout 'data: $'
+gnokey query params/vm/gno.land/r/sys/setter.baz.int64
+stdout 'data: $'
+
+gnokey maketx addpkg -pkgdir $WORK/setter -pkgpath gno.land/r/sys/setter -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
+
+# query after adding the package, but before setting values
+gnokey query params/vm/gno.land/r/sys/setter.foo.string
+stdout 'data: $'
+gnokey query params/vm/gno.land/r/sys/setter.bar.bool
+stdout 'data: $'
+gnokey query params/vm/gno.land/r/sys/setter.baz.int64
+stdout 'data: $'
+
+
+# set foo (string)
+gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetFoo -args foo1 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+gnokey query params/vm/gno.land/r/sys/setter.foo.string
+stdout 'data: "foo1"'
+
+# override foo
+gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetFoo -args foo2 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+gnokey query params/vm/gno.land/r/sys/setter.foo.string
+stdout 'data: "foo2"'
+
+
+# set bar (bool)
+gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBar -args true -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+gnokey query params/vm/gno.land/r/sys/setter.bar.bool
+stdout 'data: true'
+
+# override bar
+gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBar -args false -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+gnokey query params/vm/gno.land/r/sys/setter.bar.bool
+stdout 'data: false'
+
+
+# set baz (bool)
+gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBaz -args 1337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+gnokey query params/vm/gno.land/r/sys/setter.baz.int64
+stdout 'data: "1337"'
+
+# override baz
+gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBaz -args 31337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+gnokey query params/vm/gno.land/r/sys/setter.baz.int64
+stdout 'data: "31337"'
+
+-- setter/setter.gno --
+package setter
+
+import (
+ "std"
+)
+
+func SetFoo(newFoo string) { std.SetParamString("foo.string", newFoo) }
+func SetBar(newBar bool) { std.SetParamBool("bar.bool", newBar) }
+func SetBaz(newBaz int64) { std.SetParamInt64("baz.int64", newBaz) }
diff --git a/gno.land/pkg/integration/testdata/patchpkg.txtar b/gno.land/pkg/integration/testdata/patchpkg.txtar
index c5962709625..0a1a7fa993d 100644
--- a/gno.land/pkg/integration/testdata/patchpkg.txtar
+++ b/gno.land/pkg/integration/testdata/patchpkg.txtar
@@ -2,13 +2,13 @@ loadpkg gno.land/r/dev/admin $WORK
adduser dev
-patchpkg "g1abcde" $USER_ADDR_dev
+patchpkg "g1abcde" $dev_user_addr
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
+stdout $dev_user_addr
-- admin.gno --
package admin
diff --git a/gno.land/pkg/integration/testdata/prevrealm.txtar b/gno.land/pkg/integration/testdata/prevrealm.txtar
new file mode 100644
index 00000000000..31f0ca336ba
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/prevrealm.txtar
@@ -0,0 +1,184 @@
+# This tests ensure the consistency of the std.PrevRealm function, in the
+# following situations:
+#
+#
+# | Num | Msg Type | Call from | Entry Point | Result |
+# |-----|:--------:|:-------------------:|:---------------:|:------------:|
+# | 1 | MsgCall | wallet direct | myrlm.A() | user address |
+# | 2 | | | myrlm.B() | user address |
+# | 3 | | through /r/foo | myrlm.A() | r/foo |
+# | 4 | | | myrlm.B() | r/foo |
+# | 5 | | through /p/demo/bar | bar.A() | user address |
+# | 6 | | | bar.B() | user address |
+# | 7 | MsgRun | wallet direct | myrlm.A() | user address |
+# | 8 | | | myrlm.B() | user address |
+# | 9 | | through /r/foo | myrlm.A() | r/foo |
+# | 10 | | | myrlm.B() | r/foo |
+# | 11 | | through /p/demo/bar | bar.A() | user address |
+# | 12 | | | bar.B() | user address |
+# | 13 | MsgCall | wallet direct | std.PrevRealm() | user address |
+# | 14 | MsgRun | wallet direct | std.PrevRealm() | user address |
+
+# Init
+## deploy myrlm
+loadpkg gno.land/r/myrlm $WORK/r/myrlm
+## deploy r/foo
+loadpkg gno.land/r/foo $WORK/r/foo
+## deploy p/demo/bar
+loadpkg gno.land/p/demo/bar $WORK/p/demo/bar
+
+## start a new node
+gnoland start
+
+env RFOO_USER_ADDR=g1evezrh92xaucffmtgsaa3rvmz5s8kedffsg469
+
+# Test cases
+## 1. MsgCall -> myrlm.A: user address
+gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 1500000 -broadcast -chainid tendermint_test test1
+stdout ${test1_user_addr}
+
+## 2. MsgCall -> myrealm.B -> myrlm.A: user address
+gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 800000 -broadcast -chainid tendermint_test test1
+stdout ${test1_user_addr}
+
+## 3. MsgCall -> r/foo.A -> myrlm.A: r/foo
+gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 800000 -broadcast -chainid tendermint_test test1
+stdout ${RFOO_USER_ADDR}
+
+## 4. MsgCall -> r/foo.B -> myrlm.B -> r/foo.A: r/foo
+gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 800000 -broadcast -chainid tendermint_test test1
+stdout ${RFOO_USER_ADDR}
+
+## remove due to update to maketx call can only call realm (case 5, 6, 13)
+## 5. MsgCall -> p/demo/bar.A: user address
+## gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1
+## stdout ${test1_user_addr}
+
+## 6. MsgCall -> p/demo/bar.B: user address
+## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1
+## stdout ${test1_user_addr}
+
+## 7. MsgRun -> myrlm.A: user address
+gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno
+stdout ${test1_user_addr}
+
+## 8. MsgRun -> myrealm.B -> myrlm.A: user address
+gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno
+stdout ${test1_user_addr}
+
+## 9. MsgRun -> r/foo.A -> myrlm.A: r/foo
+gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno
+stdout ${RFOO_USER_ADDR}
+
+## 10. MsgRun -> r/foo.B -> myrlm.B -> r/foo.A: r/foo
+gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno
+stdout ${RFOO_USER_ADDR}
+
+## 11. MsgRun -> p/demo/bar.A -> myrlm.A: user address
+gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno
+stdout ${test1_user_addr}
+
+## 12. MsgRun -> p/demo/bar.B -> myrlm.B -> r/foo.A: user address
+gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno
+stdout ${test1_user_addr}
+
+## 13. MsgCall -> std.PrevRealm(): user address
+## gnokey maketx call -pkgpath std -func PrevRealm -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1
+## stdout ${test1_user_addr}
+
+## 14. MsgRun -> std.PrevRealm(): user address
+gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno
+stdout ${test1_user_addr}
+
+-- r/myrlm/myrlm.gno --
+package myrlm
+
+import "std"
+
+func A() string {
+ return std.PrevRealm().Addr().String()
+}
+
+func B() string {
+ return A()
+}
+-- r/foo/foo.gno --
+package foo
+
+import "gno.land/r/myrlm"
+
+func A() string {
+ return myrlm.A()
+}
+
+func B() string {
+ return myrlm.B()
+}
+-- p/demo/bar/bar.gno --
+package bar
+
+import "std"
+
+func A() string {
+ return std.PrevRealm().Addr().String()
+}
+
+func B() string {
+ return A()
+}
+-- run/myrlmA.gno --
+package main
+
+import myrlm "gno.land/r/myrlm"
+
+func main() {
+ println(myrlm.A())
+}
+-- run/myrlmB.gno --
+package main
+
+import "gno.land/r/myrlm"
+
+func main() {
+ println(myrlm.B())
+}
+-- run/fooA.gno --
+package main
+
+import "gno.land/r/foo"
+
+func main() {
+ println(foo.A())
+}
+-- run/fooB.gno --
+package main
+
+import "gno.land/r/foo"
+
+func main() {
+ println(foo.B())
+}
+-- run/barA.gno --
+package main
+
+import "gno.land/p/demo/bar"
+
+func main() {
+ println(bar.A())
+}
+-- run/barB.gno --
+package main
+
+import "gno.land/p/demo/bar"
+
+func main() {
+ println(bar.B())
+}
+-- run/baz.gno --
+package main
+
+import "std"
+
+func main() {
+ println(std.PrevRealm().Addr().String())
+}
diff --git a/gno.land/pkg/integration/testdata/realm_banker_issued_coin_denom.txtar b/gno.land/pkg/integration/testdata/realm_banker_issued_coin_denom.txtar
new file mode 100644
index 00000000000..a55604267ae
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/realm_banker_issued_coin_denom.txtar
@@ -0,0 +1,124 @@
+# test for https://github.com/gnolang/gno/pull/875
+
+## another test user, test2
+adduser test2
+
+## start a new node
+gnoland start
+
+## add realm_banker
+gnokey maketx addpkg -pkgdir $WORK/short -pkgpath gno.land/r/test/realm_banker -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
+
+## add realm_banker with long package_name
+gnokey maketx addpkg -pkgdir $WORK/long -pkgpath gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890 -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
+
+## add invalid realm_denom
+gnokey maketx addpkg -pkgdir $WORK/invalid_realm_denom -pkgpath gno.land/r/test/invalid_realm_denom -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
+
+## test2 spend all balance
+gnokey maketx send -send "9999999ugnot" -to g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test2
+
+## check test2 balance
+gnokey query bank/balances/${test2_user_addr}
+stdout ''
+
+## mint coin from banker
+gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Mint -args ${test2_user_addr} -args "ugnot" -args "31337" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+
+## check balance after minting, without patching banker will return '31337ugnot'
+gnokey query bank/balances/${test2_user_addr}
+stdout '"31337/gno.land/r/test/realm_banker:ugnot"'
+
+## burn coin
+gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Burn -args ${test2_user_addr} -args "ugnot" -args "7" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+
+## check balance after burning
+gnokey query bank/balances/${test2_user_addr}
+stdout '"31330/gno.land/r/test/realm_banker:ugnot"'
+
+## transfer 1ugnot to test2 for gas-fee of below tx
+gnokey maketx send -send "1ugnot" -to ${test2_user_addr} -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+
+## transfer coin
+gnokey maketx send -send "1330/gno.land/r/test/realm_banker:ugnot" -to g1yr0dpfgthph7y6mepdx8afuec4q3ga2lg8tjt0 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test2
+
+## check sender balance
+gnokey query bank/balances/${test2_user_addr}
+stdout '"30000/gno.land/r/test/realm_banker:ugnot"'
+
+## check receiver balance
+gnokey query bank/balances/g1yr0dpfgthph7y6mepdx8afuec4q3ga2lg8tjt0
+stdout '"1330/gno.land/r/test/realm_banker:ugnot"'
+
+## mint coin from long named package with banker
+gnokey maketx call -pkgpath gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890 -func Mint -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "ugnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+gnokey query bank/balances/g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7
+stdout '"100/gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890:ugnot"'
+
+## mint invalid base denom
+! gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Mint -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "2gnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+stderr 'cannot issue coins with invalid denom base name, it should start by a lowercase letter and be followed by 2-15 lowercase letters or digits'
+
+## burn invalid base denom
+! gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Burn -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "2gnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+stderr 'cannot issue coins with invalid denom base name, it should start by a lowercase letter and be followed by 2-15 lowercase letters or digits'
+
+## mint invalid realm denom
+! gnokey maketx call -pkgpath gno.land/r/test/invalid_realm_denom -func Mint -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "ugnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+stderr 'invalid denom, can only issue/remove coins with the realm.s prefix'
+
+## burn invalid realm denom
+! gnokey maketx call -pkgpath gno.land/r/test/invalid_realm_denom -func Burn -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "ugnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+stderr 'invalid denom, can only issue/remove coins with the realm.s prefix'
+
+-- short/realm_banker.gno --
+package realm_banker
+
+import (
+ "std"
+)
+
+func Mint(addr std.Address, denom string, amount int64) {
+ banker := std.GetBanker(std.BankerTypeRealmIssue)
+ banker.IssueCoin(addr, std.CurrentRealm().CoinDenom(denom), amount)
+}
+
+func Burn(addr std.Address, denom string, amount int64) {
+ banker := std.GetBanker(std.BankerTypeRealmIssue)
+ banker.RemoveCoin(addr, std.CurrentRealm().CoinDenom(denom), amount)
+}
+
+-- long/realm_banker.gno --
+// package name is 130 characters long
+package package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890
+
+import (
+ "std"
+)
+
+func Mint(addr std.Address, denom string, amount int64) {
+ banker := std.GetBanker(std.BankerTypeRealmIssue)
+ banker.IssueCoin(addr, std.CurrentRealm().CoinDenom(denom), amount)
+}
+
+func Burn(addr std.Address, denom string, amount int64) {
+ banker := std.GetBanker(std.BankerTypeRealmIssue)
+ banker.RemoveCoin(addr, std.CurrentRealm().CoinDenom(denom), amount)
+}
+
+-- invalid_realm_denom/realm_banker.gno --
+package invalid_realm_denom
+
+import (
+ "std"
+)
+
+func Mint(addr std.Address, denom string, amount int64) {
+ banker := std.GetBanker(std.BankerTypeRealmIssue)
+ banker.IssueCoin(addr, denom, amount)
+}
+
+func Burn(addr std.Address, denom string, amount int64) {
+ banker := std.GetBanker(std.BankerTypeRealmIssue)
+ banker.RemoveCoin(addr, denom, amount)
+}
diff --git a/gno.land/pkg/integration/testdata/restart.txtar b/gno.land/pkg/integration/testdata/restart.txtar
new file mode 100644
index 00000000000..5571aa9fa66
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/restart.txtar
@@ -0,0 +1,23 @@
+# simple test for the `gnoland restart` command;
+# should restart the gno.land node and recover state.
+
+loadpkg gno.land/r/demo/counter $WORK
+gnoland start
+
+gnokey maketx call -pkgpath gno.land/r/demo/counter -func Incr -gas-fee 1000000ugnot -gas-wanted 200000 -broadcast -chainid tendermint_test test1
+stdout '\(1 int\)'
+
+gnoland restart
+
+gnokey maketx call -pkgpath gno.land/r/demo/counter -func Incr -gas-fee 1000000ugnot -gas-wanted 200000 -broadcast -chainid tendermint_test test1
+stdout '\(2 int\)'
+
+-- counter.gno --
+package counter
+
+var counter int
+
+func Incr() int {
+ counter++
+ return counter
+}
diff --git a/gno.land/pkg/integration/testdata/restart_missing_type.txtar b/gno.land/pkg/integration/testdata/restart_missing_type.txtar
new file mode 100644
index 00000000000..cc8ed702734
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/restart_missing_type.txtar
@@ -0,0 +1,205 @@
+# add a random user
+adduserfrom user1 'bone make joy hospital hawk crew civil relief maple alter always frozen category emerge fun inflict room sphere casino vital scheme basket omit wrap'
+stdout 'g1lmgyf29g6zqgpln5pq05zzt7qkz2wga7xgagv4'
+
+# This txtar is a regression test for a bug, whereby a type is committed to
+# the defaultStore.cacheTypes map, but not to the underlying store (due to a
+# failing transaction).
+# For more information: https://github.com/gnolang/gno/pull/2605
+loadpkg gno.land/p/demo/avl
+gnoland start
+
+gnokey sign -tx-path $WORK/tx1.tx -chainid tendermint_test -account-sequence 0 -account-number 57 user1
+! gnokey broadcast $WORK/tx1.tx
+stderr 'out of gas'
+
+gnokey sign -tx-path $WORK/tx2.tx -chainid tendermint_test -account-sequence 1 -account-number 57 user1
+gnokey broadcast $WORK/tx2.tx
+stdout 'OK!'
+
+gnokey sign -tx-path $WORK/tx3.tx -chainid tendermint_test -account-sequence 2 -account-number 57 user1
+gnokey broadcast $WORK/tx3.tx
+stdout 'OK!'
+
+gnoland restart
+
+-- tx1.tx --
+{
+ "msg": [
+ {
+ "@type": "/vm.m_addpkg",
+ "creator": "g1lmgyf29g6zqgpln5pq05zzt7qkz2wga7xgagv4",
+ "package": {
+ "name": "zentasktic",
+ "path": "gno.land/p/g17ernafy6ctpcz6uepfsq2js8x2vz0wladh5yc3/zentasktic",
+ "files": [
+ {
+ "name": "README.md",
+ "body": "# ZenTasktic Core\n\nA basic, minimalisitc Asess-Decide-Do implementations as `p/zentasktic`. The diagram below shows a simplified ADD workflow.\n\n![ZenTasktic](ZenTasktic-framework.png)\n\nThis implementation will expose all the basic features of the framework: tasks & projects with complete workflows. Ideally, this should offer all the necessary building blocks for any other custom implementation.\n\n## Object Definitions and Default Values\n\nAs an unopinionated ADD workflow, `zentastic_core` defines the following objects:\n\n- Realm\n\nRealms act like containers for tasks & projects during their journey from Assess to Do, via Decide. Each realm has a certain restrictions, e.g. a task's Body can only be edited in Assess, a Context, Due date and Alert can only be added in Decide, etc.\n\nIf someone observes different realms, there is support for adding and removing arbitrary Realms.\n\n_note: the Ids between 1 and 4 are reserved for: 1-Assess, 2-Decide, 3-Do, 4-Collection. Trying to add or remove such a Realm will raise an error._\n\n\nRealm data definition:\n\n```\ntype Realm struct {\n\tId \t\t\tstring `json:\"realmId\"`\n\tName \t\tstring `json:\"realmName\"`\n}\n```\n\n- Task\n\nA task is the minimal data structure in ZenTasktic, with the following definition:\n\n```\ntype Task struct {\n\tId \t\t\tstring `json:\"taskId\"`\n\tProjectId \tstring `json:\"taskProjectId\"`\n\tContextId\tstring `json:\"taskContextId\"`\n\tRealmId \tstring `json:\"taskRealmId\"`\n\tBody \t\tstring `json:\"taskBody\"`\n\tDue\t\t\tstring `json:\"taskDue\"`\n\tAlert\t\tstring `json:\"taskAlert\"`\n}\n```\n\n- Project\n\nProjects are unopinionated collections of Tasks. A Task in a Project can be in any Realm, but the restrictions are propagated upwards to the Project: e.g. if a Task is marked as 'done' in the Do realm (namely changing its RealmId property to \"1\", Assess, or \"4\" Collection), and the rest of the tasks are not, the Project cannot be moved back to Decide or Asses, all Tasks must have consisted RealmId properties.\n\nA Task can be arbitrarily added to, removed from and moved to another Project.\n\nProject data definition:\n\n\n```\ntype Project struct {\n\tId \t\t\tstring `json:\"projectId\"`\n\tContextId\tstring `json:\"projectContextId\"`\n\tRealmId \tstring `json:\"projectRealmId\"`\n\tTasks\t\t[]Task `json:\"projectTasks\"`\n\tBody \t\tstring `json:\"projectBody\"`\n\tDue\t\t\tstring `json:\"ProjectDue\"`\n}\n```\n\n\n- Context\n\nContexts act as tags, grouping together Tasks and Project, e.g. \"Backend\", \"Frontend\", \"Marketing\". Contexts have no defaults and can be added or removed arbitrarily.\n\nContext data definition:\n\n```\ntype Context struct {\n\tId \t\t\tstring `json:\"contextId\"`\n\tName \t\tstring `json:\"contextName\"`\n}\n```\n\n- Collection\n\nCollections are intended as an agnostic storage for Tasks & Projects which are either not ready to be Assessed, or they have been already marked as done, and, for whatever reason, they need to be kept in the system. There is a special Realm Id for Collections, \"4\", although technically they are not part of the Assess-Decide-Do workflow.\n\nCollection data definition:\n\n```\ntype Collection struct {\n\tId \t\t\tstring `json:\"collectionId\"`\n\tRealmId \tstring `json:\"collectionRealmId\"`\n\tName \t\tstring `json:\"collectionName\"`\n\tTasks\t\t[]Task `json:\"collectionTasks\"`\n\tProjects\t[]Project `json:\"collectionProjects\"`\n}\n```\n\n- ObjectPath\n\nObjectPaths are minimalistic representations of the journey taken by a Task or a Project in the Assess-Decide-Do workflow. By recording their movement between various Realms, one can extract their `ZenStatus`, e.g., if a Task has been moved many times between Assess and Decide, never making it to Do, we can infer the following:\n-- either the Assess part was incomplete\n-- the resources needed for that Task are not yet ready\n\nObjectPath data definition:\n\n```\ntype ObjectPath struct {\n\tObjectType\tstring `json:\"objectType\"` // Task, Project\n\tId \t\t\tstring `json:\"id\"` // this is the Id of the object moved, Task, Project\n\tRealmId \tstring `json:\"realmId\"`\n}\n```\n\n_note: the core implementation offers the basic adding and retrieving functionality, but it's up to the client realm using the `zentasktic` package to call them when an object is moved from one Realm to another._\n\n## Example Workflow\n\n```\npackage example_zentasktic\n\nimport \"gno.land/p/demo/zentasktic\"\n\nvar ztm *zentasktic.ZTaskManager\nvar zpm *zentasktic.ZProjectManager\nvar zrm *zentasktic.ZRealmManager\nvar zcm *zentasktic.ZContextManager\nvar zcl *zentasktic.ZCollectionManager\nvar zom *zentasktic.ZObjectPathManager\n\nfunc init() {\n ztm = zentasktic.NewZTaskManager()\n zpm = zentasktic.NewZProjectManager()\n\tzrm = zentasktic.NewZRealmManager()\n\tzcm = zentasktic.NewZContextManager()\n\tzcl = zentasktic.NewZCollectionManager()\n\tzom = zentasktic.NewZObjectPathManager()\n}\n\n// initializing a task, assuming we get the value POSTed by some call to the current realm\n\nnewTask := zentasktic.Task{Id: \"20\", Body: \"Buy milk\"}\nztm.AddTask(newTask)\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"1\"}\nzom.AddPath(taskPath)\n...\n\neditedTask := zentasktic.Task{Id: \"20\", Body: \"Buy fresh milk\"}\nztm.EditTask(editedTask)\n\n...\n\n// moving it to Decide\n\nztm.MoveTaskToRealm(\"20\", \"2\")\n\n// adding context, due date and alert, assuming they're received from other calls\n\nshoppingContext := zcm.GetContextById(\"2\")\n\ncerr := zcm.AddContextToTask(ztm, shoppingContext, editedTask)\n\nderr := ztm.SetTaskDueDate(editedTask.Id, \"2024-04-10\")\nnow := time.Now() // replace with the actual time of the alert\nalertTime := now.Format(\"2006-01-02 15:04:05\")\naerr := ztm.SetTaskAlert(editedTask.Id, alertTime)\n\n...\n\n// move the Task to Do\n\nztm.MoveTaskToRealm(editedTask.Id, \"2\")\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"2\"}\nzom.AddPath(taskPath)\n\n// after the task is done, we sent it back to Assess\n\nztm.MoveTaskToRealm(editedTask.Id,\"1\")\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"1\"}\nzom.AddPath(taskPath)\n\n// from here, we can add it to a collection\n\nmyCollection := zcm.GetCollectionById(\"1\")\n\nzcm.AddTaskToCollection(ztm, myCollection, editedTask)\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"4\"}\nzom.AddPath(taskPath)\n\n```\n\nAll tests are in the `*_test.gno` files, e.g. `tasks_test.gno`, `projects_test.gno`, etc."
+ },
+ {
+ "name": "collections.gno",
+ "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\n\ntype Collection struct {\n\tId \t\t\tstring `json:\"collectionId\"`\n\tRealmId \tstring `json:\"collectionRealmId\"`\n\tName \t\tstring `json:\"collectionName\"`\n\tTasks\t\t[]Task `json:\"collectionTasks\"`\n\tProjects\t[]Project `json:\"collectionProjects\"`\n}\n\ntype ZCollectionManager struct {\n\tCollections *avl.Tree \n\tCollectionTasks *avl.Tree\n\tCollectionProjects *avl.Tree \n}\n\nfunc NewZCollectionManager() *ZCollectionManager {\n return &ZCollectionManager{\n Collections: avl.NewTree(),\n CollectionTasks: avl.NewTree(),\n CollectionProjects: avl.NewTree(),\n }\n}\n\n\n// actions\n\nfunc (zcolm *ZCollectionManager) AddCollection(c Collection) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif exist {\n\t\t\treturn ErrCollectionIdAlreadyExists\n\t\t}\n\t}\n\tzcolm.Collections.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) EditCollection(c Collection) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\t\n\tzcolm.Collections.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) RemoveCollection(c Collection) (err error) {\n // implementation\n if zcolm.Collections.Size() != 0 {\n collectionInterface, exist := zcolm.Collections.Get(c.Id)\n if !exist {\n return ErrCollectionIdNotFound\n }\n collection := collectionInterface.(Collection)\n\n _, removed := zcolm.Collections.Remove(collection.Id)\n if !removed {\n return ErrCollectionNotRemoved\n }\n\n if zcolm.CollectionTasks.Size() != 0 {\n _, removedTasks := zcolm.CollectionTasks.Remove(collection.Id)\n if !removedTasks {\n return ErrCollectionNotRemoved\n }\t\n }\n\n if zcolm.CollectionProjects.Size() != 0 {\n _, removedProjects := zcolm.CollectionProjects.Remove(collection.Id)\n if !removedProjects {\n return ErrCollectionNotRemoved\n }\t\n }\n }\n return nil\n}\n\n\nfunc (zcolm *ZCollectionManager) AddProjectToCollection(zpm *ZProjectManager, c Collection, p Project) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionProjects, texist := zcolm.CollectionProjects.Get(c.Id)\n\tif !texist {\n\t\t// If the collections has no projects yet, initialize the slice.\n\t\texistingCollectionProjects = []Project{}\n\t} else {\n\t\tprojects, ok := existingCollectionProjects.([]Project)\n\t\tif !ok {\n\t\t\treturn ErrCollectionsProjectsNotFound\n\t\t}\n\t\texistingCollectionProjects = projects\n\t}\n\tp.RealmId = \"4\"\n\tif err := zpm.EditProject(p); err != nil {\n\t\treturn err\n\t}\n\tupdatedProjects := append(existingCollectionProjects.([]Project), p)\n\tzcolm.CollectionProjects.Set(c.Id, updatedProjects)\n\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) AddTaskToCollection(ztm *ZTaskManager, c Collection, t Task) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionTasks, texist := zcolm.CollectionTasks.Get(c.Id)\n\tif !texist {\n\t\t// If the collections has no tasks yet, initialize the slice.\n\t\texistingCollectionTasks = []Task{}\n\t} else {\n\t\ttasks, ok := existingCollectionTasks.([]Task)\n\t\tif !ok {\n\t\t\treturn ErrCollectionsTasksNotFound\n\t\t}\n\t\texistingCollectionTasks = tasks\n\t}\n\tt.RealmId = \"4\"\n\tif err := ztm.EditTask(t); err != nil {\n\t\treturn err\n\t}\n\tupdatedTasks := append(existingCollectionTasks.([]Task), t)\n\tzcolm.CollectionTasks.Set(c.Id, updatedTasks)\n\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) RemoveProjectFromCollection(zpm *ZProjectManager, c Collection, p Project) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionProjects, texist := zcolm.CollectionProjects.Get(c.Id)\n\tif !texist {\n\t\t// If the collection has no projects yet, return appropriate error\n\t\treturn ErrCollectionsProjectsNotFound\n\t}\n\n\t// Find the index of the project to be removed.\n\tvar index int = -1\n\tfor i, project := range existingCollectionProjects.([]Project) {\n\t\tif project.Id == p.Id {\n\t\t\tindex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// If the project was found, we remove it from the slice.\n\tif index != -1 {\n\t\t// by default we send it back to Assess\n\t\tp.RealmId = \"1\"\n\t\tzpm.EditProject(p)\n\t\texistingCollectionProjects = append(existingCollectionProjects.([]Project)[:index], existingCollectionProjects.([]Project)[index+1:]...)\n\t} else {\n\t\t// Project not found in the collection\n\t\treturn ErrProjectByIdNotFound \n\t}\n\tzcolm.CollectionProjects.Set(c.Id, existingCollectionProjects)\n\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) RemoveTaskFromCollection(ztm *ZTaskManager, c Collection, t Task) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionTasks, texist := zcolm.CollectionTasks.Get(c.Id)\n\tif !texist {\n\t\t// If the collection has no tasks yet, return appropriate error\n\t\treturn ErrCollectionsTasksNotFound\n\t}\n\n\t// Find the index of the task to be removed.\n\tvar index int = -1\n\tfor i, task := range existingCollectionTasks.([]Task) {\n\t\tif task.Id == t.Id {\n\t\t\tindex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// If the task was found, we remove it from the slice.\n\tif index != -1 {\n\t\t// by default, we send the task to Assess\n\t\tt.RealmId = \"1\"\n\t\tztm.EditTask(t)\n\t\texistingCollectionTasks = append(existingCollectionTasks.([]Task)[:index], existingCollectionTasks.([]Task)[index+1:]...)\n\t} else {\n\t\t// Task not found in the collection\n\t\treturn ErrTaskByIdNotFound \n\t}\n\tzcolm.CollectionTasks.Set(c.Id, existingCollectionTasks)\n\n\treturn nil\n}\n\n// getters\n\nfunc (zcolm *ZCollectionManager) GetCollectionById(collectionId string) (Collection, error) {\n if zcolm.Collections.Size() != 0 {\n cInterface, exist := zcolm.Collections.Get(collectionId)\n if exist {\n collection := cInterface.(Collection)\n // look for collection Tasks, Projects\n existingCollectionTasks, texist := zcolm.CollectionTasks.Get(collectionId)\n if texist {\n collection.Tasks = existingCollectionTasks.([]Task)\n }\n existingCollectionProjects, pexist := zcolm.CollectionProjects.Get(collectionId)\n if pexist {\n collection.Projects = existingCollectionProjects.([]Project)\n }\n return collection, nil\n }\n return Collection{}, ErrCollectionByIdNotFound\n }\n return Collection{}, ErrCollectionByIdNotFound\n}\n\nfunc (zcolm *ZCollectionManager) GetCollectionTasks(c Collection) (tasks []Task, err error) {\n\t\n\tif zcolm.CollectionTasks.Size() != 0 {\n\t\ttask, exist := zcolm.CollectionTasks.Get(c.Id)\n\t\tif !exist {\n\t\t\t// if there's no record in CollectionTasks, we don't have to return anything\n\t\t\treturn nil, ErrCollectionsTasksNotFound\n\t\t} else {\n\t\t\t// type assertion to convert interface{} to []Task\n\t\t\texistingCollectionTasks, ok := task.([]Task)\n\t\t\tif !ok {\n\t\t\t\treturn nil, ErrTaskFailedToAssert\n\t\t\t}\n\t\t\treturn existingCollectionTasks, nil\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc (zcolm *ZCollectionManager) GetCollectionProjects(c Collection) (projects []Project, err error) {\n\t\n\tif zcolm.CollectionProjects.Size() != 0 {\n\t\tproject, exist := zcolm.CollectionProjects.Get(c.Id)\n\t\tif !exist {\n\t\t\t// if there's no record in CollectionProjets, we don't have to return anything\n\t\t\treturn nil, ErrCollectionsProjectsNotFound\n\t\t} else {\n\t\t\t// type assertion to convert interface{} to []Projet\n\t\t\texistingCollectionProjects, ok := project.([]Project)\n\t\t\tif !ok {\n\t\t\t\treturn nil, ErrProjectFailedToAssert\n\t\t\t}\n\t\t\treturn existingCollectionProjects, nil\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc (zcolm *ZCollectionManager) GetAllCollections() (collections string, err error) {\n\t// implementation\n\tvar allCollections []Collection\n\t\n\t// Iterate over the Collections AVL tree to collect all Project objects.\n\t\n\tzcolm.Collections.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif collection, ok := value.(Collection); ok {\n\t\t\t// get collection tasks, if any\n\t\t\tcollectionTasks, _ := zcolm.GetCollectionTasks(collection)\n\t\t\tif collectionTasks != nil {\n\t\t\t\tcollection.Tasks = collectionTasks\n\t\t\t}\n\t\t\t// get collection prokects, if any\n\t\t\tcollectionProjects, _ := zcolm.GetCollectionProjects(collection)\n\t\t\tif collectionProjects != nil {\n\t\t\t\tcollection.Projects = collectionProjects\n\t\t\t}\n\t\t\tallCollections = append(allCollections, collection)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a CollectionsObject with all collected tasks.\n\tcollectionsObject := CollectionsObject{\n\t\tCollections: allCollections,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the collections into JSON.\n\tmarshalledCollections, merr := collectionsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\", merr\n\t} \n\treturn string(marshalledCollections), nil\n} "
+ },
+ {
+ "name": "collections_test.gno",
+ "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n/*\n\nfunc Test_AddCollection(t *testing.T) {\n \n collection := Collection{Id: \"1\", RealmId: \"4\", Name: \"First collection\",}\n\n // Test adding a collection successfully.\n err := collection.AddCollection()\n if err != nil {\n t.Errorf(\"Failed to add collection: %v\", err)\n }\n\n // Test adding a duplicate task.\n cerr := collection.AddCollection()\n if cerr != ErrCollectionIdAlreadyExists {\n t.Errorf(\"Expected ErrCollectionIdAlreadyExists, got %v\", cerr)\n }\n}\n\nfunc Test_RemoveCollection(t *testing.T) {\n \n collection := Collection{Id: \"20\", RealmId: \"4\", Name: \"Removable collection\",}\n\n // Test adding a collection successfully.\n err := collection.AddCollection()\n if err != nil {\n t.Errorf(\"Failed to add collection: %v\", err)\n }\n\n retrievedCollection, rerr := GetCollectionById(collection.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added collection\")\n }\n\n // Test removing a collection\n terr := retrievedCollection.RemoveCollection()\n if terr != ErrCollectionNotRemoved {\n t.Errorf(\"Expected ErrCollectionNotRemoved, got %v\", terr)\n }\n}\n\nfunc Test_EditCollection(t *testing.T) {\n \n collection := Collection{Id: \"2\", RealmId: \"4\", Name: \"Second collection\",}\n\n // Test adding a collection successfully.\n err := collection.AddCollection()\n if err != nil {\n t.Errorf(\"Failed to add collection: %v\", err)\n }\n\n // Test editing the collection\n editedCollection := Collection{Id: collection.Id, RealmId: collection.RealmId, Name: \"Edited collection\",}\n cerr := editedCollection.EditCollection()\n if cerr != nil {\n t.Errorf(\"Failed to edit the collection\")\n }\n\n retrievedCollection, _ := GetCollectionById(editedCollection.Id)\n if retrievedCollection.Name != \"Edited collection\" {\n t.Errorf(\"Collection was not edited\")\n }\n}\n\nfunc Test_AddProjectToCollection(t *testing.T){\n // Example Collection and Projects\n col := Collection{Id: \"1\", Name: \"First collection\", RealmId: \"4\",}\n prj := Project{Id: \"10\", Body: \"Project 10\", RealmId: \"1\",}\n\n Collections.Set(col.Id, col) // Mock existing collections\n\n tests := []struct {\n name string\n collection Collection\n project Project\n wantErr bool\n errMsg error\n }{\n {\n name: \"Attach to existing collection\",\n collection: col,\n project: prj,\n wantErr: false,\n },\n {\n name: \"Attach to non-existing collection\",\n collection: Collection{Id: \"200\", Name: \"Collection 200\", RealmId: \"4\",},\n project: prj,\n wantErr: true,\n errMsg: ErrCollectionIdNotFound,\n },\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n err := tt.collection.AddProjectToCollection(tt.project)\n if (err != nil) != tt.wantErr {\n t.Errorf(\"AddProjectToCollection() error = %v, wantErr %v\", err, tt.wantErr)\n }\n if tt.wantErr && err != tt.errMsg {\n t.Errorf(\"AddProjectToCollection() error = %v, expected %v\", err, tt.errMsg)\n }\n\n // For successful attach, verify the project is added to the collection's tasks.\n if !tt.wantErr {\n projects, exist := CollectionProjects.Get(tt.collection.Id)\n if !exist || len(projects.([]Project)) == 0 {\n t.Errorf(\"Project was not added to the collection\")\n } else {\n found := false\n for _, project := range projects.([]Project) {\n if project.Id == tt.project.Id {\n found = true\n break\n }\n }\n if !found {\n t.Errorf(\"Project was not attached to the collection\")\n }\n }\n }\n })\n }\n}\n\nfunc Test_AddTaskToCollection(t *testing.T){\n // Example Collection and Tasks\n col := Collection{Id: \"2\", Name: \"Second Collection\", RealmId: \"4\",}\n tsk := Task{Id: \"30\", Body: \"Task 30\", RealmId: \"1\",}\n\n Collections.Set(col.Id, col) // Mock existing collections\n\n tests := []struct {\n name string\n collection Collection\n task Task\n wantErr bool\n errMsg error\n }{\n {\n name: \"Attach to existing collection\",\n collection: col,\n task: tsk,\n wantErr: false,\n },\n {\n name: \"Attach to non-existing collection\",\n collection: Collection{Id: \"210\", Name: \"Collection 210\", RealmId: \"4\",},\n task: tsk,\n wantErr: true,\n errMsg: ErrCollectionIdNotFound,\n },\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n err := tt.collection.AddTaskToCollection(tt.task)\n if (err != nil) != tt.wantErr {\n t.Errorf(\"AddTaskToCollection() error = %v, wantErr %v\", err, tt.wantErr)\n }\n if tt.wantErr && err != tt.errMsg {\n t.Errorf(\"AddTaskToCollection() error = %v, expected %v\", err, tt.errMsg)\n }\n\n // For successful attach, verify the task is added to the collection's tasks.\n if !tt.wantErr {\n tasks, exist := CollectionTasks.Get(tt.collection.Id)\n if !exist || len(tasks.([]Task)) == 0 {\n t.Errorf(\"Task was not added to the collection\")\n } else {\n found := false\n for _, task := range tasks.([]Task) {\n if task.Id == tt.task.Id {\n found = true\n break\n }\n }\n if !found {\n t.Errorf(\"Task was not attached to the collection\")\n }\n }\n }\n })\n }\n}\n\nfunc Test_RemoveProjectFromCollection(t *testing.T){\n // Setup:\n\tcollection := Collection{Id: \"300\", Name: \"Collection 300\",}\n\tproject1 := Project{Id: \"21\", Body: \"Project 21\", RealmId: \"1\",}\n\tproject2 := Project{Id: \"22\", Body: \"Project 22\", RealmId: \"1\",}\n\n collection.AddCollection()\n project1.AddProject()\n project2.AddProject()\n collection.AddProjectToCollection(project1)\n collection.AddProjectToCollection(project2)\n\n\ttests := []struct {\n\t\tname string\n\t\tproject Project\n\t\tcollection Collection\n\t\twantErr bool\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"Remove existing project from collection\",\n\t\t\tproject: project1,\n\t\t\tcollection: collection,\n\t\t\twantErr: false,\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove project from non-existing collection\",\n\t\t\tproject: project1,\n\t\t\tcollection: Collection{Id: \"nonexistent\"},\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrCollectionIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove non-existing project from collection\",\n\t\t\tproject: Project{Id: \"nonexistent\"},\n\t\t\tcollection: collection,\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrProjectByIdNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.collection.RemoveProjectFromCollection(tt.project)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil || err != tt.expectedErr {\n\t\t\t\t\tt.Errorf(\"%s: expected error %v, got %v\", tt.name, tt.expectedErr, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"%s: unexpected error: %v\", tt.name, err)\n\t\t\t\t}\n\n\t\t\t\t// For successful removal, verify the project is no longer part of the collection's projects\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\tprojects, _ := CollectionProjects.Get(tt.collection.Id)\n\t\t\t\t\tfor _, project := range projects.([]Project) {\n\t\t\t\t\t\tif project.Id == tt.project.Id {\n\t\t\t\t\t\t\tt.Errorf(\"%s: project was not detached from the collection\", tt.name)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_RemoveTaskFromCollection(t *testing.T){\n // setup, re-using parts from Test_AddTaskToCollection\n\tcollection := Collection{Id: \"40\", Name: \"Collection 40\",}\n task1 := Task{Id: \"40\", Body: \"Task 40\", RealmId: \"1\",}\n\n collection.AddCollection()\n task1.AddTask()\n collection.AddTaskToCollection(task1)\n\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\tcollection Collection\n\t\twantErr bool\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"Remove existing task from collection\",\n\t\t\ttask: task1,\n\t\t\tcollection: collection,\n\t\t\twantErr: false,\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove task from non-existing collection\",\n\t\t\ttask: task1,\n\t\t\tcollection: Collection{Id: \"nonexistent\"},\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrCollectionIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove non-existing task from collection\",\n\t\t\ttask: Task{Id: \"nonexistent\"},\n\t\t\tcollection: collection,\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrTaskByIdNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.collection.RemoveTaskFromCollection(tt.task)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil || err != tt.expectedErr {\n\t\t\t\t\tt.Errorf(\"%s: expected error %v, got %v\", tt.name, tt.expectedErr, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"%s: unexpected error: %v\", tt.name, err)\n\t\t\t\t}\n\n\t\t\t\t// For successful removal, verify the task is no longer part of the collection's tasks\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\ttasks, _ := CollectionTasks.Get(tt.collection.Id)\n\t\t\t\t\tfor _, task := range tasks.([]Task) {\n\t\t\t\t\t\tif task.Id == tt.task.Id {\n\t\t\t\t\t\t\tt.Errorf(\"%s: task was not detached from the collection\", tt.name)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_GetCollectionById(t *testing.T){\n // test getting a non-existing collection\n nonCollection, err := GetCollectionById(\"0\")\n if err != ErrCollectionByIdNotFound {\n t.Fatalf(\"Expected ErrCollectionByIdNotFound, got: %v\", err)\n }\n\n // test getting the correct collection by id\n correctCollection, err := GetCollectionById(\"1\")\n if err != nil {\n t.Fatalf(\"Failed to get collection by id, error: %v\", err)\n }\n\n if correctCollection.Name != \"First collection\" {\n t.Fatalf(\"Got the wrong collection, with name: %v\", correctCollection.Name)\n }\n}\n\nfunc Test_GetCollectionTasks(t *testing.T) {\n // retrieving objects based on these mocks\n //col := Collection{Id: \"2\", Name: \"Second Collection\", RealmId: \"4\",}\n tsk := Task{Id: \"30\", Body: \"Task 30\", RealmId: \"1\",}\n\n collection, cerr := GetCollectionById(\"2\")\n if cerr != nil {\n t.Errorf(\"GetCollectionById() failed, %v\", cerr)\n }\n\n collectionTasks, pterr := collection.GetCollectionTasks()\n if len(collectionTasks) == 0 {\n t.Errorf(\"GetCollectionTasks() failed, %v\", pterr)\n }\n\n // test detaching from an existing collection\n dtterr := collection.RemoveTaskFromCollection(tsk)\n if dtterr != nil {\n t.Errorf(\"RemoveTaskFromCollection() failed, %v\", dtterr)\n }\n\n collectionWithNoTasks, pterr := collection.GetCollectionTasks()\n if len(collectionWithNoTasks) != 0 {\n t.Errorf(\"GetCollectionTasks() after detach failed, %v\", pterr)\n }\n\n // add task back to collection, for tests mockup integrity\n collection.AddTaskToCollection(tsk)\n}\n\nfunc Test_GetCollectionProjects(t *testing.T) {\n // retrieving objects based on these mocks\n //col := Collection{Id: \"1\", Name: \"First Collection\", RealmId: \"4\",}\n prj := Project{Id: \"10\", Body: \"Project 10\", RealmId: \"2\", ContextId: \"2\", Due: \"2024-01-01\"}\n\n collection, cerr := GetCollectionById(\"1\")\n if cerr != nil {\n t.Errorf(\"GetCollectionById() failed, %v\", cerr)\n }\n\n collectionProjects, pterr := collection.GetCollectionProjects()\n if len(collectionProjects) == 0 {\n t.Errorf(\"GetCollectionProjects() failed, %v\", pterr)\n }\n\n // test detaching from an existing collection\n dtterr := collection.RemoveProjectFromCollection(prj)\n if dtterr != nil {\n t.Errorf(\"RemoveProjectFromCollection() failed, %v\", dtterr)\n }\n\n collectionWithNoProjects, pterr := collection.GetCollectionProjects()\n if len(collectionWithNoProjects) != 0 {\n t.Errorf(\"GetCollectionProjects() after detach failed, %v\", pterr)\n }\n\n // add project back to collection, for tests mockup integrity\n collection.AddProjectToCollection(prj)\n}\n\nfunc Test_GetAllCollections(t *testing.T){\n // mocking the collections based on previous tests\n // TODO: add isolation?\n knownCollections := []Collection{\n {\n Id: \"1\",\n RealmId: \"4\",\n Name: \"First collection\",\n Tasks: nil, \n Projects: []Project{\n {\n Id: \"10\",\n ContextId: \"2\",\n RealmId: \"4\",\n Tasks: nil, \n Body: \"Project 10\",\n Due: \"2024-01-01\",\n },\n },\n },\n {\n Id: \"2\",\n RealmId: \"4\",\n Name: \"Second Collection\",\n Tasks: []Task{\n {\n Id:\"30\",\n ProjectId:\"\",\n ContextId:\"\",\n RealmId:\"4\",\n Body:\"Task 30\",\n Due:\"\",\n Alert:\"\",\n },\n },\n Projects: nil, \n },\n {\n Id:\"20\",\n RealmId:\"4\",\n Name:\"Removable collection\",\n Tasks: nil,\n Projects: nil,\n },\n {\n Id: \"300\",\n Name: \"Collection 300\",\n Tasks: nil, \n Projects: []Project {\n {\n Id:\"22\",\n ContextId:\"\",\n RealmId:\"4\",\n Tasks: nil,\n Body:\"Project 22\",\n Due:\"\",\n },\n }, \n },\n {\n Id: \"40\",\n Name: \"Collection 40\",\n Tasks: nil, \n Projects: nil, \n },\n }\n \n\n // Manually marshal the known collections to create the expected outcome.\n collectionsObject := CollectionsObject{Collections: knownCollections}\n expected, err := collectionsObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known collections: %v\", err)\n }\n\n // Execute GetAllCollections() to get the actual outcome.\n actual, err := GetAllCollections()\n if err != nil {\n t.Fatalf(\"GetAllCollections() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual collections JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n\n\n\n\n"
+ },
+ {
+ "name": "contexts.gno",
+ "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\ntype Context struct {\n\tId string `json:\"contextId\"`\n\tName string `json:\"contextName\"`\n}\n\ntype ZContextManager struct {\n\tContexts *avl.Tree\n}\n\nfunc NewZContextManager() *ZContextManager {\n\treturn &ZContextManager{\n\t\tContexts: avl.NewTree(),\n\t}\n}\n\n// Actions\n\nfunc (zcm *ZContextManager) AddContext(c Context) error {\n\tif zcm.Contexts.Size() != 0 {\n\t\t_, exist := zcm.Contexts.Get(c.Id)\n\t\tif exist {\n\t\t\treturn ErrContextIdAlreadyExists\n\t\t}\n\t}\n\tzcm.Contexts.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) EditContext(c Context) error {\n\tif zcm.Contexts.Size() != 0 {\n\t\t_, exist := zcm.Contexts.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrContextIdNotFound\n\t\t}\n\t}\n\tzcm.Contexts.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) RemoveContext(c Context) error {\n\tif zcm.Contexts.Size() != 0 {\n\t\tcontext, exist := zcm.Contexts.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrContextIdNotFound\n\t\t}\n\t\t_, removed := zcm.Contexts.Remove(context.(Context).Id)\n\t\tif !removed {\n\t\t\treturn ErrContextNotRemoved\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) AddContextToTask(ztm *ZTaskManager, c Context, t Task) error {\n\ttaskInterface, exist := ztm.Tasks.Get(t.Id)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\t_, cexist := zcm.Contexts.Get(c.Id)\n\tif !cexist {\n\t\treturn ErrContextIdNotFound\n\t}\n\n\tif t.RealmId == \"2\" {\n\t\ttask := taskInterface.(Task)\n\t\ttask.ContextId = c.Id\n\t\tztm.Tasks.Set(t.Id, task)\n\t} else {\n\t\treturn ErrTaskNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) AddContextToProject(zpm *ZProjectManager, c Context, p Project) error {\n\tprojectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\t_, cexist := zcm.Contexts.Get(c.Id)\n\tif !cexist {\n\t\treturn ErrContextIdNotFound\n\t}\n\n\tif p.RealmId == \"2\" {\n\t\tproject := projectInterface.(Project)\n\t\tproject.ContextId = c.Id\n\t\tzpm.Projects.Set(p.Id, project)\n\t} else {\n\t\treturn ErrProjectNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) AddContextToProjectTask(zpm *ZProjectManager, c Context, p Project, projectTaskId string) error {\n\t\n\t_, cexist := zcm.Contexts.Get(c.Id)\n\tif !cexist {\n\t\treturn ErrContextIdNotFound\n\t}\n\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n\tif existingProject.RealmId != \"2\" {\n\t\treturn ErrProjectNotEditable\n\t}\n\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(p.Id)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n existingProject.Tasks = tasks\n\n var index int = -1\n for i, task := range existingProject.Tasks {\n if task.Id == projectTaskId {\n index = i\n break\n }\n }\n\n if index != -1 {\n existingProject.Tasks[index].ContextId = c.Id\n } else {\n return ErrTaskByIdNotFound\n }\n\n zpm.ProjectTasks.Set(p.Id, existingProject.Tasks)\n return nil\n}\n\n// getters\n\nfunc (zcm *ZContextManager) GetContextById(contextId string) (Context, error) {\n\tif zcm.Contexts.Size() != 0 {\n\t\tcInterface, exist := zcm.Contexts.Get(contextId)\n\t\tif exist {\n\t\t\treturn cInterface.(Context), nil\n\t\t}\n\t\treturn Context{}, ErrContextIdNotFound\n\t}\n\treturn Context{}, ErrContextIdNotFound\n}\n\nfunc (zcm *ZContextManager) GetAllContexts() (string) {\n\tvar allContexts []Context\n\n\t// Iterate over the Contexts AVL tree to collect all Context objects.\n\tzcm.Contexts.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif context, ok := value.(Context); ok {\n\t\t\tallContexts = append(allContexts, context)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ContextsObject with all collected contexts.\n\tcontextsObject := &ContextsObject{\n\t\tContexts: allContexts,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the contexts into JSON.\n\tmarshalledContexts, merr := contextsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t}\n\treturn string(marshalledContexts)\n}\n\n"
+ },
+ {
+ "name": "contexts_test.gno",
+ "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n/*\nfunc Test_AddContext(t *testing.T) {\n \n context := Context{Id: \"1\", Name: \"Work\"}\n\n // Test adding a context successfully.\n err := context.AddContext()\n if err != nil {\n t.Errorf(\"Failed to add context: %v\", err)\n }\n\n // Test adding a duplicate task.\n cerr := context.AddContext()\n if cerr != ErrContextIdAlreadyExists {\n t.Errorf(\"Expected ErrContextIdAlreadyExists, got %v\", cerr)\n }\n}\n\nfunc Test_EditContext(t *testing.T) {\n \n context := Context{Id: \"2\", Name: \"Home\"}\n\n // Test adding a context successfully.\n err := context.AddContext()\n if err != nil {\n t.Errorf(\"Failed to add context: %v\", err)\n }\n\n // Test editing the context\n editedContext := Context{Id: \"2\", Name: \"Shopping\"}\n cerr := editedContext.EditContext()\n if cerr != nil {\n t.Errorf(\"Failed to edit the context\")\n }\n\n retrievedContext, _ := GetContextById(editedContext.Id)\n if retrievedContext.Name != \"Shopping\" {\n t.Errorf(\"Context was not edited\")\n }\n}\n\nfunc Test_RemoveContext(t *testing.T) {\n \n context := Context{Id: \"4\", Name: \"Gym\",}\n\n // Test adding a context successfully.\n err := context.AddContext()\n if err != nil {\n t.Errorf(\"Failed to add context: %v\", err)\n }\n\n retrievedContext, rerr := GetContextById(context.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added context\")\n }\n // Test removing a context\n cerr := retrievedContext.RemoveContext()\n if cerr != ErrContextNotRemoved {\n t.Errorf(\"Expected ErrContextNotRemoved, got %v\", cerr)\n }\n}\n\nfunc Test_AddContextToTask(t *testing.T) {\n\n task := Task{Id: \"10\", Body: \"First content\", RealmId: \"2\", ContextId: \"1\",}\n\n // Test adding a task successfully.\n err := task.AddTask()\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n taskInDecide, exist := Tasks.Get(\"10\")\n\tif !exist {\n\t\tt.Errorf(\"Task with id 10 not found\")\n\t}\n\t// check if context exists\n\tcontextToAdd, cexist := Contexts.Get(\"2\")\n\tif !cexist {\n\t\tt.Errorf(\"Context with id 2 not found\")\n\t}\n\n derr := contextToAdd.(Context).AddContextToTask(taskInDecide.(Task))\n if derr != nil {\n t.Errorf(\"Could not add context to a task in Decide, err %v\", derr)\n }\n}\n\nfunc Test_AddContextToProject(t *testing.T) {\n\n project := Project{Id: \"10\", Body: \"Project 10\", RealmId: \"2\", ContextId: \"1\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n projectInDecide, exist := Projects.Get(\"10\")\n\tif !exist {\n\t\tt.Errorf(\"Project with id 10 not found\")\n\t}\n\t// check if context exists\n\tcontextToAdd, cexist := Contexts.Get(\"2\")\n\tif !cexist {\n\t\tt.Errorf(\"Context with id 2 not found\")\n\t}\n\n derr := contextToAdd.(Context).AddContextToProject(projectInDecide.(Project))\n if derr != nil {\n t.Errorf(\"Could not add context to a project in Decide, err %v\", derr)\n }\n}\n\nfunc Test_GetAllContexts(t *testing.T) {\n \n // mocking the contexts based on previous tests\n // TODO: add isolation?\n knownContexts := []Context{\n {Id: \"1\", Name: \"Work\",},\n {Id: \"2\", Name: \"Shopping\",},\n {Id: \"4\", Name: \"Gym\",},\n }\n\n // Manually marshal the known contexts to create the expected outcome.\n contextsObject := ContextsObject{Contexts: knownContexts}\n expected, err := contextsObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known contexts: %v\", err)\n }\n\n // Execute GetAllContexts() to get the actual outcome.\n actual, err := GetAllContexts()\n if err != nil {\n t.Fatalf(\"GetAllContexts() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual contexts JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n\n"
+ },
+ {
+ "name": "core.gno",
+ "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n// holding the path of an object since creation\n// each time we move an object from one realm to another, we add to its path\ntype ObjectPath struct {\n\tObjectType string `json:\"objectType\"` // Task, Project\n\tId string `json:\"id\"` // this is the Id of the object moved, Task, Project\n\tRealmId string `json:\"realmId\"`\n}\n\ntype ZObjectPathManager struct {\n\tPaths avl.Tree\n\tPathId int\n}\n\nfunc NewZObjectPathManager() *ZObjectPathManager {\n\treturn &ZObjectPathManager{\n\t\tPaths: *avl.NewTree(),\n\t\tPathId: 1,\n\t}\n}\n\nfunc (zopm *ZObjectPathManager) AddPath(o ObjectPath) error {\n\tzopm.PathId++\n\tupdated := zopm.Paths.Set(strconv.Itoa(zopm.PathId), o)\n\tif !updated {\n\t\treturn ErrObjectPathNotUpdated\n\t}\n\treturn nil\n}\n\nfunc (zopm *ZObjectPathManager) GetObjectJourney(objectType string, objectId string) (string, error) {\n\tvar objectPaths []ObjectPath\n\n\t// Iterate over the Paths AVL tree to collect all ObjectPath objects.\n\tzopm.Paths.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif objectPath, ok := value.(ObjectPath); ok {\n\t\t\tif objectPath.ObjectType == objectType && objectPath.Id == objectId {\n\t\t\t\tobjectPaths = append(objectPaths, objectPath)\n\t\t\t}\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create an ObjectJourney with all collected paths.\n\tobjectJourney := &ObjectJourney{\n\t\tObjectPaths: objectPaths,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the journey into JSON.\n\tmarshalledJourney, merr := objectJourney.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\", merr\n\t}\n\treturn string(marshalledJourney), nil\n}\n\n\n// GetZenStatus\n/* todo: leave it to the client\nfunc () GetZenStatus() (zenStatus string, err error) {\n\t// implementation\n}\n*/\n"
+ },
+ {
+ "name": "errors.gno",
+ "body": "package zentasktic\n\nimport \"errors\"\n\nvar (\n\tErrTaskNotEditable \t= errors.New(\"Task is not editable\")\n\tErrProjectNotEditable = errors.New(\"Project is not editable\")\n\tErrProjectIdNotFound\t\t\t= errors.New(\"Project id not found\")\n\tErrTaskIdNotFound\t\t\t\t= errors.New(\"Task id not found\")\n\tErrTaskFailedToAssert\t\t\t= errors.New(\"Failed to assert Task type\")\n\tErrProjectFailedToAssert\t\t= errors.New(\"Failed to assert Project type\")\n\tErrProjectTasksNotFound\t\t\t= errors.New(\"Could not get tasks for project\")\n\tErrCollectionsProjectsNotFound\t= errors.New(\"Could not get projects for this collection\")\n\tErrCollectionsTasksNotFound\t\t= errors.New(\"Could not get tasks for this collection\")\n\tErrTaskIdAlreadyExists\t\t\t= errors.New(\"A task with the provided id already exists\")\n\tErrCollectionIdAlreadyExists\t= errors.New(\"A collection with the provided id already exists\")\n\tErrProjectIdAlreadyExists\t\t= errors.New(\"A project with the provided id already exists\")\n\tErrTaskByIdNotFound\t\t\t\t= errors.New(\"Can't get task by id\")\n\tErrProjectByIdNotFound\t\t\t= errors.New(\"Can't get project by id\")\n\tErrCollectionByIdNotFound\t\t= errors.New(\"Can't get collection by id\")\n\tErrTaskNotRemovable\t\t\t\t= errors.New(\"Cannot remove a task directly from this realm\")\n\tErrProjectNotRemovable\t\t\t= errors.New(\"Cannot remove a project directly from this realm\")\n\tErrProjectTasksNotRemoved\t\t= errors.New(\"Project tasks were not removed\")\n\tErrTaskNotRemoved\t\t\t\t= errors.New(\"Task was not removed\")\n\tErrTaskNotInAssessRealm\t\t\t= errors.New(\"Task is not in Assess, cannot edit Body\")\n\tErrProjectNotInAssessRealm\t\t= errors.New(\"Project is not in Assess, cannot edit Body\")\n\tErrContextIdAlreadyExists\t\t= errors.New(\"A context with the provided id already exists\")\n\tErrContextIdNotFound\t\t\t= errors.New(\"Context id not found\")\n\tErrCollectionIdNotFound\t\t\t= errors.New(\"Collection id not found\")\n\tErrContextNotRemoved\t\t\t= errors.New(\"Context was not removed\")\n\tErrProjectNotRemoved\t\t\t= errors.New(\"Project was not removed\")\n\tErrCollectionNotRemoved\t\t\t= errors.New(\"Collection was not removed\")\n\tErrObjectPathNotUpdated\t\t\t= errors.New(\"Object path wasn't updated\")\n\tErrInvalidateDateFormat\t\t\t= errors.New(\"Invalida date format\")\n\tErrInvalidDateFilterType\t\t= errors.New(\"Invalid date filter type\")\n\tErrRealmIdAlreadyExists\t\t\t= errors.New(\"A realm with the same id already exists\")\n\tErrRealmIdNotAllowed\t\t\t= errors.New(\"This is a reserved realm id\")\n\tErrRealmIdNotFound\t\t\t\t= errors.New(\"Realm id not found\")\n\tErrRealmNotRemoved\t\t\t\t= errors.New(\"Realm was not removed\")\n)"
+ },
+ {
+ "name": "marshals.gno",
+ "body": "package zentasktic\n\nimport (\n\t\"bytes\"\n)\n\n\ntype ContextsObject struct {\n\tContexts\t[]Context\n}\n\ntype TasksObject struct {\n\tTasks\t[]Task\n}\n\ntype ProjectsObject struct {\n\tProjects\t[]Project\n}\n\ntype CollectionsObject struct {\n\tCollections\t[]Collection\n}\n\ntype RealmsObject struct {\n\tRealms\t[]Realm\n}\n\ntype ObjectJourney struct {\n\tObjectPaths []ObjectPath\n}\n\nfunc (c Context) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"contextId\":\"`)\n\tb.WriteString(c.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"contextName\":\"`)\n\tb.WriteString(c.Name)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (cs ContextsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"contexts\":[`)\n\t\n\tfor i, context := range cs.Contexts {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcontextJSON, cerr := context.MarshalJSON()\n\t\tif cerr == nil {\n\t\t\tb.WriteString(string(contextJSON))\n\t\t}\n\t}\n\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (t Task) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"taskId\":\"`)\n\tb.WriteString(t.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskProjectId\":\"`)\n\tb.WriteString(t.ProjectId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskContextId\":\"`)\n\tb.WriteString(t.ContextId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskRealmId\":\"`)\n\tb.WriteString(t.RealmId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskBody\":\"`)\n\tb.WriteString(t.Body)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskDue\":\"`)\n\tb.WriteString(t.Due)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskAlert\":\"`)\n\tb.WriteString(t.Alert)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (ts TasksObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"tasks\":[`)\n\tfor i, task := range ts.Tasks {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\ttaskJSON, cerr := task.MarshalJSON()\n\t\tif cerr == nil {\n\t\t\tb.WriteString(string(taskJSON))\n\t\t}\n\t}\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (p Project) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"projectId\":\"`)\n\tb.WriteString(p.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectContextId\":\"`)\n\tb.WriteString(p.ContextId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectRealmId\":\"`)\n\tb.WriteString(p.RealmId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectTasks\":[`)\n\n\tif len(p.Tasks) != 0 {\n\t\tfor i, projectTask := range p.Tasks {\n\t\t\tif i > 0 {\n\t\t\t\tb.WriteString(`,`)\n\t\t\t}\n\t\t\tprojectTaskJSON, perr := projectTask.MarshalJSON()\n\t\t\tif perr == nil {\n\t\t\t\tb.WriteString(string(projectTaskJSON))\n\t\t\t}\n\t\t}\n\t}\n\n\tb.WriteString(`],`)\n\n\tb.WriteString(`\"projectBody\":\"`)\n\tb.WriteString(p.Body)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectDue\":\"`)\n\tb.WriteString(p.Due)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (ps ProjectsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"projects\":[`)\n\tfor i, project := range ps.Projects {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tprojectJSON, perr := project.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(projectJSON))\n\t\t}\n\t}\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (c Collection) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"collectionId\":\"`)\n\tb.WriteString(c.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"collectionRealmId\":\"`)\n\tb.WriteString(c.RealmId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"collectionName\":\"`)\n\tb.WriteString(c.Name)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"collectionTasks\":[`)\n\tfor i, collectionTask := range c.Tasks {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcollectionTaskJSON, perr := collectionTask.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(collectionTaskJSON))\n\t\t}\n\t}\n\tb.WriteString(`],`)\n\n\tb.WriteString(`\"collectionProjects\":[`)\n\tfor i, collectionProject := range c.Projects {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcollectionProjectJSON, perr := collectionProject.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(collectionProjectJSON))\n\t\t}\n\t}\n\tb.WriteString(`],`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (co CollectionsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"collections\":[`)\n\tfor i, collection := range co.Collections {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcollectionJSON, perr := collection.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(collectionJSON))\n\t\t}\n\t}\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (r Realm) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"realmId\":\"`)\n\tb.WriteString(r.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"realmName\":\"`)\n\tb.WriteString(r.Name)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (rs RealmsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"realms\":[`)\n\t\n\tfor i, realm := range rs.Realms {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\trealmJSON, rerr := realm.MarshalJSON()\n\t\tif rerr == nil {\n\t\t\tb.WriteString(string(realmJSON))\n\t\t}\n\t}\n\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (op ObjectPath) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"objectType\":\"`)\n\tb.WriteString(op.ObjectType)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"id\":\"`)\n\tb.WriteString(op.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"realmId\":\"`)\n\tb.WriteString(op.RealmId)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (oj ObjectJourney) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"objectJourney\":[`)\n\t\n\tfor i, objectPath := range oj.ObjectPaths {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tobjectPathJSON, oerr := objectPath.MarshalJSON()\n\t\tif oerr == nil {\n\t\t\tb.WriteString(string(objectPathJSON))\n\t\t}\n\t}\n\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n"
+ },
+ {
+ "name": "projects.gno",
+ "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n\ntype Project struct {\n\tId \t\t\tstring `json:\"projectId\"`\n\tContextId\tstring `json:\"projectContextId\"`\n\tRealmId \tstring `json:\"projectRealmId\"`\n\tTasks\t\t[]Task `json:\"projectTasks\"`\n\tBody \t\tstring `json:\"projectBody\"`\n\tDue\t\t\tstring `json:\"projectDue\"`\n}\n\ntype ZProjectManager struct {\n\tProjects *avl.Tree // projectId -> Project\n\tProjectTasks *avl.Tree // projectId -> []Task\n}\n\n\nfunc NewZProjectManager() *ZProjectManager {\n\treturn &ZProjectManager{\n\t\tProjects: avl.NewTree(),\n\t\tProjectTasks: avl.NewTree(),\n\t}\n}\n\n// actions\n\nfunc (zpm *ZProjectManager) AddProject(p Project) (err error) {\n\t// implementation\n\n\tif zpm.Projects.Size() != 0 {\n\t\t_, exist := zpm.Projects.Get(p.Id)\n\t\tif exist {\n\t\t\treturn ErrProjectIdAlreadyExists\n\t\t}\n\t}\n\tzpm.Projects.Set(p.Id, p)\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) RemoveProject(p Project) (err error) {\n\t// implementation, remove from ProjectTasks too\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\t // project is removable only in Asses (RealmId 1) or via a Collection (RealmId 4)\n\tif existingProject.RealmId != \"1\" && existingProject.RealmId != \"4\" {\n\t\treturn ErrProjectNotRemovable\n\t}\n\n\t_, removed := zpm.Projects.Remove(existingProject.Id)\n\tif !removed {\n\t\treturn ErrProjectNotRemoved\n\t}\n\n\t// manage project tasks, if any\n\n\tif zpm.ProjectTasks.Size() != 0 {\n\t\t_, exist := zpm.ProjectTasks.Get(existingProject.Id)\n\t\tif !exist {\n\t\t\t// if there's no record in ProjectTasks, we don't have to remove anything\n\t\t\treturn nil\n\t\t} else {\n\t\t\t_, removed := zpm.ProjectTasks.Remove(existingProject.Id)\n\t\t\tif !removed {\n\t\t\t\treturn ErrProjectTasksNotRemoved\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) EditProject(p Project) (err error) {\n\t// implementation, get project by Id and replace the object\n\t// this is for the project body and realm, project tasks are managed in the Tasks object\n\texistingProject := Project{}\n\tif zpm.Projects.Size() != 0 {\n\t\t_, exist := zpm.Projects.Get(p.Id)\n\t\tif !exist {\n\t\t\treturn ErrProjectIdNotFound\n\t\t}\n\t}\n\t\n\t// project Body is editable only when project is in Assess, RealmId = \"1\"\n\tif p.RealmId != \"1\" {\n\t\tif p.Body != existingProject.Body {\n\t\t\treturn ErrProjectNotInAssessRealm\n\t\t}\n\t}\n\n\tzpm.Projects.Set(p.Id, p)\n\treturn nil\n}\n\n// helper function, we can achieve the same with EditProject() above\n/*func (zpm *ZProjectManager) MoveProjectToRealm(projectId string, realmId string) (err error) {\n\t// implementation\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\texistingProject.RealmId = realmId\n\tzpm.Projects.Set(projectId, existingProject)\n\treturn nil\n}*/\n\nfunc (zpm *ZProjectManager) MoveProjectToRealm(projectId string, realmId string) error {\n\t// Get the existing project from the Projects map\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\t// Set the project's RealmId to the new RealmId\n\texistingProject.RealmId = realmId\n\n\t// Get the existing project tasks from the ProjectTasks map\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n\tif !texist {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\ttasks, ok := existingProjectTasksInterface.([]Task)\n\tif !ok {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\n\t// Iterate through the project's tasks and set their RealmId to the new RealmId\n\tfor i := range tasks {\n\t\ttasks[i].RealmId = realmId\n\t}\n\n\t// Set the updated tasks back into the ProjectTasks map\n\tzpm.ProjectTasks.Set(projectId, tasks)\n\n\t// Set the updated project back into the Projects map\n\tzpm.Projects.Set(projectId, existingProject)\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) MarkProjectTaskAsDone(projectId string, projectTaskId string) error {\n // Get the existing project from the Projects map\n existingProjectInterface, exist := zpm.Projects.Get(projectId)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n // Get the existing project tasks from the ProjectTasks map\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n\n // Iterate through the project's tasks to find the task to be updated\n var taskFound bool\n for i, task := range tasks {\n if task.Id == projectTaskId {\n tasks[i].RealmId = \"4\" // Change the RealmId to \"4\"\n taskFound = true\n break\n }\n }\n\n if !taskFound {\n return ErrTaskByIdNotFound\n }\n\n // Set the updated tasks back into the ProjectTasks map\n zpm.ProjectTasks.Set(existingProject.Id, tasks)\n\n return nil\n}\n\n\nfunc (zpm *ZProjectManager) GetProjectTasks(p Project) (tasks []Task, err error) {\n\t// implementation, query ProjectTasks and return the []Tasks object\n\tvar existingProjectTasks []Task\n\n\tif zpm.ProjectTasks.Size() != 0 {\n\t\tprojectTasksInterface, exist := zpm.ProjectTasks.Get(p.Id)\n\t\tif !exist {\n\t\t\treturn nil, ErrProjectTasksNotFound\n\t\t}\n\t\texistingProjectTasks = projectTasksInterface.([]Task)\n\t\treturn existingProjectTasks, nil\n\t}\n\treturn nil, nil\n}\n\nfunc (zpm *ZProjectManager) SetProjectDueDate(projectId string, dueDate string) (err error) {\n\tprojectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\tproject := projectInterface.(Project)\n\n\t// check to see if project is in RealmId = 2 (Decide)\n\tif project.RealmId == \"2\" {\n\t\tproject.Due = dueDate\n\t\tzpm.Projects.Set(project.Id, project)\n\t} else {\n\t\treturn ErrProjectNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) SetProjectTaskDueDate(projectId string, projectTaskId string, dueDate string) (err error){\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n\tif existingProject.RealmId != \"2\" {\n\t\treturn ErrProjectNotEditable\n\t}\n\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n existingProject.Tasks = tasks\n\n var index int = -1\n for i, task := range existingProject.Tasks {\n if task.Id == projectTaskId {\n index = i\n break\n }\n }\n\n if index != -1 {\n existingProject.Tasks[index].Due = dueDate\n } else {\n return ErrTaskByIdNotFound\n }\n\n zpm.ProjectTasks.Set(projectId, existingProject.Tasks)\n return nil\n}\n\n// getters\n\nfunc (zpm *ZProjectManager) GetProjectById(projectId string) (Project, error) {\n\tif zpm.Projects.Size() != 0 {\n\t\tpInterface, exist := zpm.Projects.Get(projectId)\n\t\tif exist {\n\t\t\treturn pInterface.(Project), nil\n\t\t}\n\t}\n\treturn Project{}, ErrProjectIdNotFound\n}\n\nfunc (zpm *ZProjectManager) GetAllProjects() (projects string) {\n\t// implementation\n\tvar allProjects []Project\n\t\n\t// Iterate over the Projects AVL tree to collect all Project objects.\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif project, ok := value.(Project); ok {\n\t\t\t// get project tasks, if any\n\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\tif projectTasks != nil {\n\t\t\t\tproject.Tasks = projectTasks\n\t\t\t}\n\t\t\tallProjects = append(allProjects, project)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: allProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n}\n\nfunc (zpm *ZProjectManager) GetProjectsByRealm(realmId string) (projects string) {\n\t// implementation\n\tvar realmProjects []Project\n\t\n\t// Iterate over the Projects AVL tree to collect all Project objects.\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif project, ok := value.(Project); ok {\n\t\t\tif project.RealmId == realmId {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\trealmProjects = append(realmProjects, project)\n\t\t\t}\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: realmProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n}\n\nfunc (zpm *ZProjectManager) GetProjectsByContextAndRealm(contextId string, realmId string) (projects string) {\n\t// implementation\n\tvar contextProjects []Project\n\t\n\t// Iterate over the Projects AVL tree to collect all Project objects.\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif project, ok := value.(Project); ok {\n\t\t\tif project.ContextId == contextId && project.RealmId == realmId {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tcontextProjects = append(contextProjects, project)\n\t\t\t}\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: contextProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n}\n\nfunc (zpm *ZProjectManager) GetProjectsByDate(projectDate string, filterType string) (projects string) {\n\t// implementation\n\tparsedDate, err:= time.Parse(\"2006-01-02\", projectDate)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tvar filteredProjects []Project\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tproject, ok := value.(Project)\n\t\tif !ok {\n\t\t\treturn false // Skip this iteration and continue.\n\t\t}\n\n\t\tstoredDate, serr := time.Parse(\"2006-01-02\", project.Due)\n\t\tif serr != nil {\n\t\t\t// Skip projects with invalid dates.\n\t\t\treturn false\n\t\t}\n\n\t\tswitch filterType {\n\t\tcase \"specific\":\n\t\t\tif storedDate.Format(\"2006-01-02\") == parsedDate.Format(\"2006-01-02\") {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tfilteredProjects = append(filteredProjects, project)\n\t\t\t}\n\t\tcase \"before\":\n\t\t\tif storedDate.Before(parsedDate) {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tfilteredProjects = append(filteredProjects, project)\n\t\t\t}\n\t\tcase \"after\":\n\t\t\tif storedDate.After(parsedDate) {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tfilteredProjects = append(filteredProjects, project)\n\t\t\t}\n\t\t}\n\n\t\treturn false // Continue iteration.\n\t})\n\n\tif len(filteredProjects) == 0 {\n\t\treturn \"\"\n\t}\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: filteredProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n\n}\n"
+ },
+ {
+ "name": "projects_test.gno",
+ "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n/*\nfunc Test_AddProject(t *testing.T) {\n \n project := Project{Id: \"1\", RealmId: \"1\", Body: \"First project\", ContextId: \"1\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n // Test adding a duplicate project.\n cerr := project.AddProject()\n if cerr != ErrProjectIdAlreadyExists {\n t.Errorf(\"Expected ErrProjectIdAlreadyExists, got %v\", cerr)\n }\n}\n\n\nfunc Test_RemoveProject(t *testing.T) {\n \n project := Project{Id: \"20\", Body: \"Removable project\", RealmId: \"1\", ContextId: \"2\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n retrievedProject, rerr := GetProjectById(project.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added project\")\n }\n\n // Test removing a project\n terr := retrievedProject.RemoveProject()\n if terr != ErrProjectNotRemoved {\n t.Errorf(\"Expected ErrProjectNotRemoved, got %v\", terr)\n }\n}\n\n\nfunc Test_EditProject(t *testing.T) {\n \n project := Project{Id: \"2\", Body: \"Second project content\", RealmId: \"1\", ContextId: \"2\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n // Test editing the project\n editedProject := Project{Id: project.Id, Body: \"Edited project content\", RealmId: project.RealmId, ContextId: \"2\",}\n cerr := editedProject.EditProject()\n if cerr != nil {\n t.Errorf(\"Failed to edit the project\")\n }\n\n retrievedProject, _ := GetProjectById(editedProject.Id)\n if retrievedProject.Body != \"Edited project content\" {\n t.Errorf(\"Project was not edited\")\n }\n}\n\n\nfunc Test_MoveProjectToRealm(t *testing.T) {\n \n project := Project{Id: \"3\", Body: \"Project id 3 content\", RealmId: \"1\", ContextId: \"1\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n // Test moving the project to another realm\n \n cerr := project.MoveProjectToRealm(\"2\")\n if cerr != nil {\n t.Errorf(\"Failed to move project to another realm\")\n }\n\n retrievedProject, _ := GetProjectById(project.Id)\n if retrievedProject.RealmId != \"2\" {\n t.Errorf(\"Project was moved to the wrong realm\")\n }\n}\n\nfunc Test_SetProjectDueDate(t *testing.T) {\n\tprojectRealmIdOne, _ := GetProjectById(\"1\")\n projectRealmIdTwo, _ := GetProjectById(\"10\") \n\t// Define test cases\n\ttests := []struct {\n\t\tname string\n\t\tproject Project\n\t\tdueDate string\n\t\twantErr error\n\t}{\n\t\t{\n\t\t\tname: \"Project does not exist\",\n\t\t\tproject: Project{Id: \"nonexistent\", RealmId: \"2\"},\n\t\t\twantErr: ErrProjectIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Project not editable due to wrong realm\",\n\t\t\tproject: projectRealmIdOne,\n\t\t\twantErr: ErrProjectNotEditable,\n\t\t},\n\t\t{\n\t\t\tname: \"Successfully set alert\",\n\t\t\tproject: projectRealmIdTwo,\n\t\t\tdueDate: \"2024-01-01\",\n\t\t\twantErr: nil,\n\t\t},\n\t}\n\n\t// Execute test cases\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.project.SetProjectDueDate(tc.dueDate)\n\n\t\t\t// Validate\n\t\t\tif err != tc.wantErr {\n\t\t\t\tt.Errorf(\"Expected error %v, got %v\", tc.wantErr, err)\n\t\t\t}\n\n\t\t\t// Additional check for the success case to ensure the due date was actually set\n\t\t\tif err == nil {\n\t\t\t\t// Fetch the task again to check if the due date was set correctly\n\t\t\t\tupdatedProject, exist := Projects.Get(tc.project.Id)\n\t\t\t\tif !exist {\n\t\t\t\t\tt.Fatalf(\"Project %v was not found after setting the due date\", tc.project.Id)\n\t\t\t\t}\n\t\t\t\tif updatedProject.(Project).Due != tc.dueDate {\n\t\t\t\t\tt.Errorf(\"Expected due date to be %v, got %v\", tc.dueDate, updatedProject.(Project).Due)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// getters\n\nfunc Test_GetAllProjects(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n knownProjects := []Project{\n {Id: \"1\", Body: \"First project\", RealmId: \"1\", ContextId: \"1\",},\n {Id: \"10\", Body: \"Project 10\", RealmId: \"2\", ContextId: \"2\", Due: \"2024-01-01\"},\n\t\t{Id: \"2\", Body: \"Edited project content\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable project\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"21\", Body: \"Project 21\", RealmId: \"1\",},\n {Id: \"22\", Body: \"Project 22\", RealmId: \"1\",},\n\t\t{Id: \"3\", Body: \"Project id 3 content\", RealmId: \"2\", ContextId: \"1\",},\n }\n\n // Manually marshal the known projects to create the expected outcome.\n projectsObject := ProjectsObject{Projects: knownProjects}\n expected, err := projectsObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known projects: %v\", err)\n }\n\n // Execute GetAllProjects() to get the actual outcome.\n actual, err := GetAllProjects()\n if err != nil {\n t.Fatalf(\"GetAllProjects() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual project JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetProjectsByDate(t *testing.T) {\n\t\n\ttests := []struct {\n\t\tname string\n\t\tprojectDate string\n\t\tfilterType string\n\t\twant string\n\t\twantErr bool\n\t}{\n\t\t{\"SpecificDate\", \"2024-01-01\", \"specific\", `{\"projects\":[{\"projectId\":\"10\",\"projectContextId\":\"2\",\"projectRealmId\":\"2\",\"projectTasks\":[],\"projectBody\":\"Project 10\",\"projectDue\":\"2024-01-01\"}]}`, false},\n\t\t{\"BeforeDate\", \"2022-04-05\", \"before\", \"\", false},\n\t\t{\"AfterDate\", \"2025-04-05\", \"after\", \"\", false},\n\t\t{\"NoMatch\", \"2002-04-07\", \"specific\", \"\", false},\n\t\t{\"InvalidDateFormat\", \"April 5, 2023\", \"specific\", \"\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := GetProjectsByDate(tt.projectDate, tt.filterType)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"GetProjectsByDate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err == nil && got != tt.want {\n\t\t\t\tt.Errorf(\"GetProjectsByDate() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_GetProjectTasks(t *testing.T){\n \n task := Task{Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",}\n\n project, perr := GetProjectById(\"1\")\n if perr != nil {\n t.Errorf(\"GetProjectById() failed, %v\", perr)\n }\n\n // test attaching to an existing project\n atterr := task.AttachTaskToProject(project)\n if atterr != nil {\n t.Errorf(\"AttachTaskToProject() failed, %v\", atterr)\n }\n\n projectTasks, pterr := project.GetProjectTasks()\n if len(projectTasks) == 0 {\n t.Errorf(\"GetProjectTasks() failed, %v\", pterr)\n }\n\n // test detaching from an existing project\n dtterr := task.DetachTaskFromProject(project)\n if dtterr != nil {\n t.Errorf(\"DetachTaskFromProject() failed, %v\", dtterr)\n }\n\n projectWithNoTasks, pterr := project.GetProjectTasks()\n if len(projectWithNoTasks) != 0 {\n t.Errorf(\"GetProjectTasks() after detach failed, %v\", pterr)\n }\n}\n\nfunc Test_GetProjectById(t *testing.T){\n // test getting a non-existing project\n nonProject, err := GetProjectById(\"0\")\n if err != ErrProjectByIdNotFound {\n t.Fatalf(\"Expected ErrProjectByIdNotFound, got: %v\", err)\n }\n\n // test getting the correct task by id\n correctProject, err := GetProjectById(\"1\")\n if err != nil {\n t.Fatalf(\"Failed to get project by id, error: %v\", err)\n }\n\n if correctProject.Body != \"First project\" {\n t.Fatalf(\"Got the wrong project, with body: %v\", correctProject.Body)\n }\n}\n\nfunc Test_GetProjectsByRealm(t *testing.T) {\n \n // mocking the projects based on previous tests\n // TODO: add isolation?\n projectsInAssessRealm := []Project{\n {Id: \"1\", Body: \"First project\", RealmId: \"1\", ContextId: \"1\",},\n\t\t{Id: \"2\", Body: \"Edited project content\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable project\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"21\", Body: \"Project 21\", RealmId: \"1\",},\n {Id: \"22\", Body: \"Project 22\", RealmId: \"1\",},\n }\n\n // Manually marshal the known projects to create the expected outcome.\n projectsObjectAssess := ProjectsObject{Projects: projectsInAssessRealm}\n expected, err := projectsObjectAssess.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal projects in Assess: %v\", err)\n }\n\n actual, err := GetProjectsByRealm(\"1\")\n if err != nil {\n t.Fatalf(\"GetProjectByRealm('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual projects JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetProjectsByContext(t *testing.T) {\n \n // mocking the projects based on previous tests\n // TODO: add isolation?\n projectsInContextOne := []Project{\n {Id: \"1\", Body: \"First project\", RealmId: \"1\", ContextId: \"1\",},\n\t\t{Id: \"3\", Body: \"Project id 3 content\", RealmId: \"2\", ContextId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n projectsObjectForContexts := ProjectsObject{Projects: projectsInContextOne}\n expected, err := projectsObjectForContexts.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal projects for ContextId 1: %v\", err)\n }\n\n actual, err := GetProjectsByContext(\"1\")\n if err != nil {\n t.Fatalf(\"GetProjectsByContext('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual project JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n\n"
+ },
+ {
+ "name": "realms.gno",
+ "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\n// structs\n\ntype Realm struct {\n\tId \t\t\tstring `json:\"realmId\"`\n\tName \t\tstring `json:\"realmName\"`\n}\n\ntype ZRealmManager struct {\n\tRealms *avl.Tree\n}\n\nfunc NewZRealmManager() *ZRealmManager {\n\tzrm := &ZRealmManager{\n\t\tRealms: avl.NewTree(),\n\t}\n\tzrm.initializeHardcodedRealms()\n\treturn zrm\n}\n\n\nfunc (zrm *ZRealmManager) initializeHardcodedRealms() {\n\thardcodedRealms := []Realm{\n\t\t{Id: \"1\", Name: \"Assess\"},\n\t\t{Id: \"2\", Name: \"Decide\"},\n\t\t{Id: \"3\", Name: \"Do\"},\n\t\t{Id: \"4\", Name: \"Collections\"},\n\t}\n\n\tfor _, realm := range hardcodedRealms {\n\t\tzrm.Realms.Set(realm.Id, realm)\n\t}\n}\n\n\nfunc (zrm *ZRealmManager) AddRealm(r Realm) (err error){\n\t// implementation\n\tif zrm.Realms.Size() != 0 {\n\t\t_, exist := zrm.Realms.Get(r.Id)\n\t\tif exist {\n\t\t\treturn ErrRealmIdAlreadyExists\n\t\t}\n\t}\n\t// check for hardcoded values\n\tif r.Id == \"1\" || r.Id == \"2\" || r.Id == \"3\" || r.Id == \"4\" {\n\t\treturn ErrRealmIdNotAllowed\n\t}\n\tzrm.Realms.Set(r.Id, r)\n\treturn nil\n\t\n}\n\nfunc (zrm *ZRealmManager) RemoveRealm(r Realm) (err error){\n\t// implementation\n\tif zrm.Realms.Size() != 0 {\n\t\t_, exist := zrm.Realms.Get(r.Id)\n\t\tif !exist {\n\t\t\treturn ErrRealmIdNotFound\n\t\t} else {\n\t\t\t// check for hardcoded values, not removable\n\t\t\tif r.Id == \"1\" || r.Id == \"2\" || r.Id == \"3\" || r.Id == \"4\" {\n\t\t\t\treturn ErrRealmIdNotAllowed\n\t\t\t}\n\t\t}\n\t}\n\t\n\t_, removed := zrm.Realms.Remove(r.Id)\n\tif !removed {\n\t\treturn ErrRealmNotRemoved\n\t}\n\treturn nil\n\t\n}\n\n// getters\nfunc (zrm *ZRealmManager) GetRealmById(realmId string) (r Realm, err error) {\n\t// implementation\n\tif zrm.Realms.Size() != 0 {\n\t\trInterface, exist := zrm.Realms.Get(realmId)\n\t\tif exist {\n\t\t\treturn rInterface.(Realm), nil\n\t\t} else {\n\t\t\treturn Realm{}, ErrRealmIdNotFound\n\t\t}\n\t}\n\treturn Realm{}, ErrRealmIdNotFound\n}\n\nfunc (zrm *ZRealmManager) GetRealms() (realms string, err error) {\n\t// implementation\n\tvar allRealms []Realm\n\n\t// Iterate over the Realms AVL tree to collect all Context objects.\n\tzrm.Realms.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif realm, ok := value.(Realm); ok {\n\t\t\tallRealms = append(allRealms, realm)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\n\t// Create a RealmsObject with all collected contexts.\n\trealmsObject := &RealmsObject{\n\t\tRealms: allRealms,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the realms into JSON.\n\tmarshalledRealms, rerr := realmsObject.MarshalJSON()\n\tif rerr != nil {\n\t\treturn \"\", rerr\n\t} \n\treturn string(marshalledRealms), nil\n}\n"
+ },
+ {
+ "name": "tasks.gno",
+ "body": "package zentasktic\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\ntype Task struct {\n\tId \t\t\tstring `json:\"taskId\"`\n\tProjectId \tstring `json:\"taskProjectId\"`\n\tContextId\tstring `json:\"taskContextId\"`\n\tRealmId \tstring `json:\"taskRealmId\"`\n\tBody \t\tstring `json:\"taskBody\"`\n\tDue\t\t\tstring `json:\"taskDue\"`\n\tAlert\t\tstring `json:\"taskAlert\"`\n}\n\ntype ZTaskManager struct {\n\tTasks *avl.Tree\n}\n\nfunc NewZTaskManager() *ZTaskManager {\n\treturn &ZTaskManager{\n\t\tTasks: avl.NewTree(),\n\t}\n}\n\n// actions\n\nfunc (ztm *ZTaskManager) AddTask(t Task) error {\n\tif ztm.Tasks.Size() != 0 {\n\t\t_, exist := ztm.Tasks.Get(t.Id)\n\t\tif exist {\n\t\t\treturn ErrTaskIdAlreadyExists\n\t\t}\n\t}\n\tztm.Tasks.Set(t.Id, t)\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) RemoveTask(t Task) error {\n\texistingTaskInterface, exist := ztm.Tasks.Get(t.Id)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\texistingTask := existingTaskInterface.(Task)\n\n\t // task is removable only in Asses (RealmId 1) or via a Collection (RealmId 4)\n\tif existingTask.RealmId != \"1\" && existingTask.RealmId != \"4\" {\n\t\treturn ErrTaskNotRemovable\n\t}\n\n\t_, removed := ztm.Tasks.Remove(existingTask.Id)\n\tif !removed {\n\t\treturn ErrTaskNotRemoved\n\t}\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) EditTask(t Task) error {\n\texistingTaskInterface, exist := ztm.Tasks.Get(t.Id)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\texistingTask := existingTaskInterface.(Task)\n\n\t// task Body is editable only when task is in Assess, RealmId = \"1\"\n\tif t.RealmId != \"1\" {\n\t\tif t.Body != existingTask.Body {\n\t\t\treturn ErrTaskNotInAssessRealm\n\t\t}\n\t}\n\n\tztm.Tasks.Set(t.Id, t)\n\treturn nil\n}\n\n// Helper function to move a task to a different realm\nfunc (ztm *ZTaskManager) MoveTaskToRealm(taskId, realmId string) error {\n\texistingTaskInterface, exist := ztm.Tasks.Get(taskId)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\texistingTask := existingTaskInterface.(Task)\n\texistingTask.RealmId = realmId\n\tztm.Tasks.Set(taskId, existingTask)\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) SetTaskDueDate(taskId, dueDate string) error {\n\ttaskInterface, exist := ztm.Tasks.Get(taskId)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\ttask := taskInterface.(Task)\n\n\tif task.RealmId == \"2\" {\n\t\ttask.Due = dueDate\n\t\tztm.Tasks.Set(task.Id, task)\n\t} else {\n\t\treturn ErrTaskNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) SetTaskAlert(taskId, alertDate string) error {\n\ttaskInterface, exist := ztm.Tasks.Get(taskId)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\ttask := taskInterface.(Task)\n\n\tif task.RealmId == \"2\" {\n\t\ttask.Alert = alertDate\n\t\tztm.Tasks.Set(task.Id, task)\n\t} else {\n\t\treturn ErrTaskNotEditable\n\t}\n\n\treturn nil\n}\n\n// tasks & projects association\n\nfunc (zpm *ZProjectManager) AttachTaskToProject(ztm *ZTaskManager, t Task, p Project) error {\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(p.Id)\n\tif !texist {\n\t\texistingProject.Tasks = []Task{}\n\t} else {\n\t\ttasks, ok := existingProjectTasksInterface.([]Task)\n\t\tif !ok {\n\t\t\treturn ErrProjectTasksNotFound\n\t\t}\n\t\texistingProject.Tasks = tasks\n\t}\n\n\tt.ProjectId = p.Id\n\t// @todo we need to remove it from Tasks if it was previously added there, then detached\n\texistingTask, err := ztm.GetTaskById(t.Id)\n\tif err == nil {\n\t\tztm.RemoveTask(existingTask)\n\t}\n\tupdatedTasks := append(existingProject.Tasks, t)\n\tzpm.ProjectTasks.Set(p.Id, updatedTasks)\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) EditProjectTask(projectTaskId string, projectTaskBody string, projectId string) error {\n existingProjectInterface, exist := zpm.Projects.Get(projectId)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n\tif existingProject.RealmId != \"1\" {\n\t\treturn ErrProjectNotEditable\n\t}\n\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n existingProject.Tasks = tasks\n\n var index int = -1\n for i, task := range existingProject.Tasks {\n if task.Id == projectTaskId {\n index = i\n break\n }\n }\n\n if index != -1 {\n existingProject.Tasks[index].Body = projectTaskBody\n } else {\n return ErrTaskByIdNotFound\n }\n\n zpm.ProjectTasks.Set(projectId, existingProject.Tasks)\n return nil\n}\n\nfunc (zpm *ZProjectManager) DetachTaskFromProject(ztm *ZTaskManager, projectTaskId string, detachedTaskId string, p Project) error {\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(p.Id)\n\tif !texist {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\ttasks, ok := existingProjectTasksInterface.([]Task)\n\tif !ok {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\texistingProject.Tasks = tasks\n\n\tvar foundTask Task\n\tvar index int = -1\n\tfor i, task := range existingProject.Tasks {\n\t\tif task.Id == projectTaskId {\n\t\t\tindex = i\n\t\t\tfoundTask = task\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif index != -1 {\n\t\texistingProject.Tasks = append(existingProject.Tasks[:index], existingProject.Tasks[index+1:]...)\n\t} else {\n\t\treturn ErrTaskByIdNotFound\n\t}\n\n\tfoundTask.ProjectId = \"\"\n\tfoundTask.Id = detachedTaskId\n\t// Tasks and ProjectTasks have different storage, if a task is detached from a Project\n\t// we add it to the Tasks storage\n\tif err := ztm.AddTask(foundTask); err != nil {\n\t\treturn err\n\t}\n\n\tzpm.ProjectTasks.Set(p.Id, existingProject.Tasks)\n\treturn nil\n}\n\n\nfunc (zpm *ZProjectManager) RemoveTaskFromProject(projectTaskId string, projectId string) error {\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n\tif !texist {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\ttasks, ok := existingProjectTasksInterface.([]Task)\n\tif !ok {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\texistingProject.Tasks = tasks\n\n\tvar index int = -1\n\tfor i, task := range existingProject.Tasks {\n\t\tif task.Id == projectTaskId {\n\t\t\tindex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif index != -1 {\n\t\texistingProject.Tasks = append(existingProject.Tasks[:index], existingProject.Tasks[index+1:]...)\n\t} else {\n\t\treturn ErrTaskByIdNotFound\n\t}\n\n\tzpm.ProjectTasks.Set(projectId, existingProject.Tasks)\n\treturn nil\n}\n\n// getters\n\nfunc (ztm *ZTaskManager) GetTaskById(taskId string) (Task, error) {\n\tif ztm.Tasks.Size() != 0 {\n\t\ttInterface, exist := ztm.Tasks.Get(taskId)\n\t\tif exist {\n\t\t\treturn tInterface.(Task), nil\n\t\t}\n\t}\n\treturn Task{}, ErrTaskIdNotFound\n}\n\nfunc (ztm *ZTaskManager) GetAllTasks() (task string) {\n\tvar allTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif task, ok := value.(Task); ok {\n\t\t\tallTasks = append(allTasks, task)\n\t\t}\n\t\treturn false\n\t})\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: allTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\t\n}\n\nfunc (ztm *ZTaskManager) GetTasksByRealm(realmId string) (tasks string) {\n\tvar realmTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif task, ok := value.(Task); ok {\n\t\t\tif task.RealmId == realmId {\n\t\t\t\trealmTasks = append(realmTasks, task)\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: realmTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\n}\n\nfunc (ztm *ZTaskManager) GetTasksByContextAndRealm(contextId string, realmId string) (tasks string) {\n\tvar contextTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif task, ok := value.(Task); ok {\n\t\t\tif task.ContextId == contextId && task.ContextId == realmId {\n\t\t\t\tcontextTasks = append(contextTasks, task)\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: contextTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\n}\n\nfunc (ztm *ZTaskManager) GetTasksByDate(taskDate string, filterType string) (tasks string) {\n\tparsedDate, err := time.Parse(\"2006-01-02\", taskDate)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tvar filteredTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\ttask, ok := value.(Task)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\t\tstoredDate, serr := time.Parse(\"2006-01-02\", task.Due)\n\t\tif serr != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tswitch filterType {\n\t\tcase \"specific\":\n\t\t\tif storedDate.Format(\"2006-01-02\") == parsedDate.Format(\"2006-01-02\") {\n\t\t\t\tfilteredTasks = append(filteredTasks, task)\n\t\t\t}\n\t\tcase \"before\":\n\t\t\tif storedDate.Before(parsedDate) {\n\t\t\t\tfilteredTasks = append(filteredTasks, task)\n\t\t\t}\n\t\tcase \"after\":\n\t\t\tif storedDate.After(parsedDate) {\n\t\t\t\tfilteredTasks = append(filteredTasks, task)\n\t\t\t}\n\t\t}\n\n\t\treturn false\n\t})\n\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: filteredTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\n}\n"
+ },
+ {
+ "name": "tasks_test.gno",
+ "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n\n// Shared instance of ZTaskManager\nvar ztm *ZTaskManager\n\nfunc init() {\n ztm = NewZTaskManager()\n}\n\nfunc Test_AddTask(t *testing.T) {\n task := Task{Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",}\n\n // Test adding a task successfully.\n err := ztm.AddTask(task)\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n // Test adding a duplicate task.\n cerr := ztm.AddTask(task)\n if cerr != ErrTaskIdAlreadyExists {\n t.Errorf(\"Expected ErrTaskIdAlreadyExists, got %v\", cerr)\n }\n}\n\nfunc Test_RemoveTask(t *testing.T) {\n \n task := Task{Id: \"20\", Body: \"Removable task\", RealmId: \"1\"}\n\n // Test adding a task successfully.\n err := ztm.AddTask(task)\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n retrievedTask, rerr := ztm.GetTaskById(task.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added task\")\n }\n\n // Test removing a task\n terr := ztm.RemoveTask(retrievedTask)\n if terr != nil {\n t.Errorf(\"Expected nil, got %v\", terr)\n }\n}\n\nfunc Test_EditTask(t *testing.T) {\n \n task := Task{Id: \"2\", Body: \"First content\", RealmId: \"1\", ContextId: \"2\"}\n\n // Test adding a task successfully.\n err := ztm.AddTask(task)\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n // Test editing the task\n editedTask := Task{Id: task.Id, Body: \"Edited content\", RealmId: task.RealmId, ContextId: \"2\"}\n cerr := ztm.EditTask(editedTask)\n if cerr != nil {\n t.Errorf(\"Failed to edit the task\")\n }\n\n retrievedTask, _ := ztm.GetTaskById(editedTask.Id)\n if retrievedTask.Body != \"Edited content\" {\n t.Errorf(\"Task was not edited\")\n }\n}\n/*\nfunc Test_MoveTaskToRealm(t *testing.T) {\n \n task := Task{Id: \"3\", Body: \"First content\", RealmId: \"1\", ContextId: \"1\"}\n\n // Test adding a task successfully.\n err := task.AddTask()\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n // Test moving the task to another realm\n \n cerr := task.MoveTaskToRealm(\"2\")\n if cerr != nil {\n t.Errorf(\"Failed to move task to another realm\")\n }\n\n retrievedTask, _ := GetTaskById(task.Id)\n if retrievedTask.RealmId != \"2\" {\n t.Errorf(\"Task was moved to the wrong realm\")\n }\n}\n\nfunc Test_AttachTaskToProject(t *testing.T) {\n \n // Example Projects and Tasks\n prj := Project{Id: \"1\", Body: \"Project 1\", RealmId: \"1\",}\n tsk := Task{Id: \"4\", Body: \"Task 4\", RealmId: \"1\",}\n\n Projects.Set(prj.Id, prj) // Mock existing project\n\n tests := []struct {\n name string\n project Project\n task Task\n wantErr bool\n errMsg error\n }{\n {\n name: \"Attach to existing project\",\n project: prj,\n task: tsk,\n wantErr: false,\n },\n {\n name: \"Attach to non-existing project\",\n project: Project{Id: \"200\", Body: \"Project 200\", RealmId: \"1\",},\n task: tsk,\n wantErr: true,\n errMsg: ErrProjectIdNotFound,\n },\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n err := tt.task.AttachTaskToProject(tt.project)\n if (err != nil) != tt.wantErr {\n t.Errorf(\"AttachTaskToProject() error = %v, wantErr %v\", err, tt.wantErr)\n }\n if tt.wantErr && err != tt.errMsg {\n t.Errorf(\"AttachTaskToProject() error = %v, expected %v\", err, tt.errMsg)\n }\n\n // For successful attach, verify the task is added to the project's tasks.\n if !tt.wantErr {\n tasks, exist := ProjectTasks.Get(tt.project.Id)\n if !exist || len(tasks.([]Task)) == 0 {\n t.Errorf(\"Task was not attached to the project\")\n } else {\n found := false\n for _, task := range tasks.([]Task) {\n if task.Id == tt.task.Id {\n found = true\n break\n }\n }\n if !found {\n t.Errorf(\"Task was not attached to the project\")\n }\n }\n }\n })\n }\n}\n\nfunc TestDetachTaskFromProject(t *testing.T) {\n\t\n\t// Setup:\n\tproject := Project{Id: \"p1\", Body: \"Test Project\"}\n\ttask1 := Task{Id: \"5\", Body: \"Task One\"}\n\ttask2 := Task{Id: \"6\", Body: \"Task Two\"}\n\n\tProjects.Set(project.Id, project)\n\tProjectTasks.Set(project.Id, []Task{task1, task2})\n\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\tproject Project\n\t\twantErr bool\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"Detach existing task from project\",\n\t\t\ttask: task1,\n\t\t\tproject: project,\n\t\t\twantErr: false,\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to detach task from non-existing project\",\n\t\t\ttask: task1,\n\t\t\tproject: Project{Id: \"nonexistent\"},\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrProjectIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to detach non-existing task from project\",\n\t\t\ttask: Task{Id: \"nonexistent\"},\n\t\t\tproject: project,\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrTaskByIdNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.task.DetachTaskFromProject(tt.project)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil || err != tt.expectedErr {\n\t\t\t\t\tt.Errorf(\"%s: expected error %v, got %v\", tt.name, tt.expectedErr, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"%s: unexpected error: %v\", tt.name, err)\n\t\t\t\t}\n\n\t\t\t\t// For successful detachment, verify the task is no longer part of the project's tasks\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\ttasks, _ := ProjectTasks.Get(tt.project.Id)\n\t\t\t\t\tfor _, task := range tasks.([]Task) {\n\t\t\t\t\t\tif task.Id == tt.task.Id {\n\t\t\t\t\t\t\tt.Errorf(\"%s: task was not detached from the project\", tt.name)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SetTaskDueDate(t *testing.T) {\n\ttaskRealmIdOne, _ := GetTaskById(\"1\")\n taskRealmIdTwo, _ := GetTaskById(\"10\") \n\t// Define test cases\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\tdueDate string\n\t\twantErr error\n\t}{\n\t\t{\n\t\t\tname: \"Task does not exist\",\n\t\t\ttask: Task{Id: \"nonexistent\", RealmId: \"2\"},\n\t\t\twantErr: ErrTaskIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Task not editable due to wrong realm\",\n\t\t\ttask: taskRealmIdOne,\n\t\t\twantErr: ErrTaskNotEditable,\n\t\t},\n\t\t{\n\t\t\tname: \"Successfully set due date\",\n\t\t\ttask: taskRealmIdTwo,\n\t\t\tdueDate: \"2023-01-01\",\n\t\t\twantErr: nil,\n\t\t},\n\t}\n\n\t// Execute test cases\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.task.SetTaskDueDate(tc.dueDate)\n\n\t\t\t// Validate\n\t\t\tif err != tc.wantErr {\n\t\t\t\tt.Errorf(\"Expected error %v, got %v\", tc.wantErr, err)\n\t\t\t}\n\n\t\t\t// Additional check for the success case to ensure the due date was actually set\n\t\t\tif err == nil {\n\t\t\t\t// Fetch the task again to check if the due date was set correctly\n\t\t\t\tupdatedTask, exist := Tasks.Get(tc.task.Id)\n\t\t\t\tif !exist {\n\t\t\t\t\tt.Fatalf(\"Task %v was not found after setting the due date\", tc.task.Id)\n\t\t\t\t}\n\t\t\t\tif updatedTask.(Task).Due != tc.dueDate {\n\t\t\t\t\tt.Errorf(\"Expected due date to be %v, got %v\", tc.dueDate, updatedTask.(Task).Due)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SetTaskAlert(t *testing.T) {\n\ttaskRealmIdOne, _ := GetTaskById(\"1\")\n taskRealmIdTwo, _ := GetTaskById(\"10\") \n\t// Define test cases\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\talertDate string\n\t\twantErr error\n\t}{\n\t\t{\n\t\t\tname: \"Task does not exist\",\n\t\t\ttask: Task{Id: \"nonexistent\", RealmId: \"2\"},\n\t\t\twantErr: ErrTaskIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Task not editable due to wrong realm\",\n\t\t\ttask: taskRealmIdOne,\n\t\t\twantErr: ErrTaskNotEditable,\n\t\t},\n\t\t{\n\t\t\tname: \"Successfully set alert\",\n\t\t\ttask: taskRealmIdTwo,\n\t\t\talertDate: \"2024-01-01\",\n\t\t\twantErr: nil,\n\t\t},\n\t}\n\n\t// Execute test cases\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.task.SetTaskAlert(tc.alertDate)\n\n\t\t\t// Validate\n\t\t\tif err != tc.wantErr {\n\t\t\t\tt.Errorf(\"Expected error %v, got %v\", tc.wantErr, err)\n\t\t\t}\n\n\t\t\t// Additional check for the success case to ensure the due date was actually set\n\t\t\tif err == nil {\n\t\t\t\t// Fetch the task again to check if the due date was set correctly\n\t\t\t\tupdatedTask, exist := Tasks.Get(tc.task.Id)\n\t\t\t\tif !exist {\n\t\t\t\t\tt.Fatalf(\"Task %v was not found after setting the due date\", tc.task.Id)\n\t\t\t\t}\n\t\t\t\tif updatedTask.(Task).Alert != tc.alertDate {\n\t\t\t\t\tt.Errorf(\"Expected due date to be %v, got %v\", tc.alertDate, updatedTask.(Task).Due)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// getters\n\nfunc Test_GetAllTasks(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n knownTasks := []Task{\n {Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",},\n {Id: \"10\", Body: \"First content\", RealmId: \"2\", ContextId: \"2\", Due: \"2023-01-01\", Alert: \"2024-01-01\"},\n {Id: \"2\", Body: \"Edited content\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable task\", RealmId: \"1\",},\n {Id: \"3\", Body: \"First content\", RealmId: \"2\", ContextId: \"1\",},\n {Id: \"40\", Body: \"Task 40\", RealmId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n tasksObject := TasksObject{Tasks: knownTasks}\n expected, err := tasksObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known tasks: %v\", err)\n }\n\n // Execute GetAllTasks() to get the actual outcome.\n actual, err := GetAllTasks()\n if err != nil {\n t.Fatalf(\"GetAllTasks() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual task JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetTasksByDate(t *testing.T) {\n\t\n\ttests := []struct {\n\t\tname string\n\t\ttaskDate string\n\t\tfilterType string\n\t\twant string\n\t\twantErr bool\n\t}{\n\t\t{\"SpecificDate\", \"2023-01-01\", \"specific\", `{\"tasks\":[{\"taskId\":\"10\",\"taskProjectId\":\"\",\"taskContextId\":\"2\",\"taskRealmId\":\"2\",\"taskBody\":\"First content\",\"taskDue\":\"2023-01-01\",\"taskAlert\":\"2024-01-01\"}]}`, false},\n\t\t{\"BeforeDate\", \"2022-04-05\", \"before\", \"\", false},\n\t\t{\"AfterDate\", \"2023-04-05\", \"after\", \"\", false},\n\t\t{\"NoMatch\", \"2002-04-07\", \"specific\", \"\", false},\n\t\t{\"InvalidDateFormat\", \"April 5, 2023\", \"specific\", \"\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := GetTasksByDate(tt.taskDate, tt.filterType)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"GetTasksByDate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err == nil && got != tt.want {\n\t\t\t\tt.Errorf(\"GetTasksByDate() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_GetTaskById(t *testing.T){\n // test getting a non-existing task\n nonTask, err := GetTaskById(\"0\")\n if err != ErrTaskByIdNotFound {\n t.Fatalf(\"Expected ErrTaskByIdNotFound, got: %v\", err)\n }\n\n // test getting the correct task by id\n correctTask, err := GetTaskById(\"1\")\n if err != nil {\n t.Fatalf(\"Failed to get task by id, error: %v\", err)\n }\n\n if correctTask.Body != \"First task\" {\n t.Fatalf(\"Got the wrong task, with body: %v\", correctTask.Body)\n }\n}\n\nfunc Test_GetTasksByRealm(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n tasksInAssessRealm := []Task{\n {Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",},\n {Id: \"2\", RealmId: \"1\", Body: \"Edited content\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable task\", RealmId: \"1\",},\n {Id: \"40\", Body: \"Task 40\", RealmId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n tasksObjectAssess := TasksObject{Tasks: tasksInAssessRealm}\n expected, err := tasksObjectAssess.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal tasks in Assess: %v\", err)\n }\n\n actual, err := GetTasksByRealm(\"1\")\n if err != nil {\n t.Fatalf(\"GetTasksByRealm('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual task JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetTasksByContext(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n tasksInContextOne := []Task{\n {Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",},\n {Id: \"3\", RealmId: \"2\", Body: \"First content\", ContextId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n tasksObjectForContexts := TasksObject{Tasks: tasksInContextOne}\n expected, err := tasksObjectForContexts.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal tasks for ContextId 1: %v\", err)\n }\n\n actual, err := GetTasksByContext(\"1\")\n if err != nil {\n t.Fatalf(\"GetTasksByContext('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual task JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n"
+ }
+ ]
+ },
+ "deposit": ""
+ }
+ ],
+ "fee": {
+ "gas_wanted": "30000000",
+ "gas_fee": "1000000ugnot"
+ },
+ "signatures": [],
+ "memo": ""
+}
+
+-- tx2.tx --
+{
+ "msg": [
+ {
+ "@type": "/vm.m_addpkg",
+ "creator": "g1lmgyf29g6zqgpln5pq05zzt7qkz2wga7xgagv4",
+ "package": {
+ "name": "zentasktic",
+ "path": "gno.land/p/g17ernafy6ctpcz6uepfsq2js8x2vz0wladh5yc3/zentasktic",
+ "files": [
+ {
+ "name": "README.md",
+ "body": "# ZenTasktic Core\n\nA basic, minimalisitc Asess-Decide-Do implementations as `p/zentasktic`. The diagram below shows a simplified ADD workflow.\n\n![ZenTasktic](ZenTasktic-framework.png)\n\nThis implementation will expose all the basic features of the framework: tasks & projects with complete workflows. Ideally, this should offer all the necessary building blocks for any other custom implementation.\n\n## Object Definitions and Default Values\n\nAs an unopinionated ADD workflow, `zentastic_core` defines the following objects:\n\n- Realm\n\nRealms act like containers for tasks & projects during their journey from Assess to Do, via Decide. Each realm has a certain restrictions, e.g. a task's Body can only be edited in Assess, a Context, Due date and Alert can only be added in Decide, etc.\n\nIf someone observes different realms, there is support for adding and removing arbitrary Realms.\n\n_note: the Ids between 1 and 4 are reserved for: 1-Assess, 2-Decide, 3-Do, 4-Collection. Trying to add or remove such a Realm will raise an error._\n\n\nRealm data definition:\n\n```\ntype Realm struct {\n\tId \t\t\tstring `json:\"realmId\"`\n\tName \t\tstring `json:\"realmName\"`\n}\n```\n\n- Task\n\nA task is the minimal data structure in ZenTasktic, with the following definition:\n\n```\ntype Task struct {\n\tId \t\t\tstring `json:\"taskId\"`\n\tProjectId \tstring `json:\"taskProjectId\"`\n\tContextId\tstring `json:\"taskContextId\"`\n\tRealmId \tstring `json:\"taskRealmId\"`\n\tBody \t\tstring `json:\"taskBody\"`\n\tDue\t\t\tstring `json:\"taskDue\"`\n\tAlert\t\tstring `json:\"taskAlert\"`\n}\n```\n\n- Project\n\nProjects are unopinionated collections of Tasks. A Task in a Project can be in any Realm, but the restrictions are propagated upwards to the Project: e.g. if a Task is marked as 'done' in the Do realm (namely changing its RealmId property to \"1\", Assess, or \"4\" Collection), and the rest of the tasks are not, the Project cannot be moved back to Decide or Asses, all Tasks must have consisted RealmId properties.\n\nA Task can be arbitrarily added to, removed from and moved to another Project.\n\nProject data definition:\n\n\n```\ntype Project struct {\n\tId \t\t\tstring `json:\"projectId\"`\n\tContextId\tstring `json:\"projectContextId\"`\n\tRealmId \tstring `json:\"projectRealmId\"`\n\tTasks\t\t[]Task `json:\"projectTasks\"`\n\tBody \t\tstring `json:\"projectBody\"`\n\tDue\t\t\tstring `json:\"ProjectDue\"`\n}\n```\n\n\n- Context\n\nContexts act as tags, grouping together Tasks and Project, e.g. \"Backend\", \"Frontend\", \"Marketing\". Contexts have no defaults and can be added or removed arbitrarily.\n\nContext data definition:\n\n```\ntype Context struct {\n\tId \t\t\tstring `json:\"contextId\"`\n\tName \t\tstring `json:\"contextName\"`\n}\n```\n\n- Collection\n\nCollections are intended as an agnostic storage for Tasks & Projects which are either not ready to be Assessed, or they have been already marked as done, and, for whatever reason, they need to be kept in the system. There is a special Realm Id for Collections, \"4\", although technically they are not part of the Assess-Decide-Do workflow.\n\nCollection data definition:\n\n```\ntype Collection struct {\n\tId \t\t\tstring `json:\"collectionId\"`\n\tRealmId \tstring `json:\"collectionRealmId\"`\n\tName \t\tstring `json:\"collectionName\"`\n\tTasks\t\t[]Task `json:\"collectionTasks\"`\n\tProjects\t[]Project `json:\"collectionProjects\"`\n}\n```\n\n- ObjectPath\n\nObjectPaths are minimalistic representations of the journey taken by a Task or a Project in the Assess-Decide-Do workflow. By recording their movement between various Realms, one can extract their `ZenStatus`, e.g., if a Task has been moved many times between Assess and Decide, never making it to Do, we can infer the following:\n-- either the Assess part was incomplete\n-- the resources needed for that Task are not yet ready\n\nObjectPath data definition:\n\n```\ntype ObjectPath struct {\n\tObjectType\tstring `json:\"objectType\"` // Task, Project\n\tId \t\t\tstring `json:\"id\"` // this is the Id of the object moved, Task, Project\n\tRealmId \tstring `json:\"realmId\"`\n}\n```\n\n_note: the core implementation offers the basic adding and retrieving functionality, but it's up to the client realm using the `zentasktic` package to call them when an object is moved from one Realm to another._\n\n## Example Workflow\n\n```\npackage example_zentasktic\n\nimport \"gno.land/p/demo/zentasktic\"\n\nvar ztm *zentasktic.ZTaskManager\nvar zpm *zentasktic.ZProjectManager\nvar zrm *zentasktic.ZRealmManager\nvar zcm *zentasktic.ZContextManager\nvar zcl *zentasktic.ZCollectionManager\nvar zom *zentasktic.ZObjectPathManager\n\nfunc init() {\n ztm = zentasktic.NewZTaskManager()\n zpm = zentasktic.NewZProjectManager()\n\tzrm = zentasktic.NewZRealmManager()\n\tzcm = zentasktic.NewZContextManager()\n\tzcl = zentasktic.NewZCollectionManager()\n\tzom = zentasktic.NewZObjectPathManager()\n}\n\n// initializing a task, assuming we get the value POSTed by some call to the current realm\n\nnewTask := zentasktic.Task{Id: \"20\", Body: \"Buy milk\"}\nztm.AddTask(newTask)\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"1\"}\nzom.AddPath(taskPath)\n...\n\neditedTask := zentasktic.Task{Id: \"20\", Body: \"Buy fresh milk\"}\nztm.EditTask(editedTask)\n\n...\n\n// moving it to Decide\n\nztm.MoveTaskToRealm(\"20\", \"2\")\n\n// adding context, due date and alert, assuming they're received from other calls\n\nshoppingContext := zcm.GetContextById(\"2\")\n\ncerr := zcm.AddContextToTask(ztm, shoppingContext, editedTask)\n\nderr := ztm.SetTaskDueDate(editedTask.Id, \"2024-04-10\")\nnow := time.Now() // replace with the actual time of the alert\nalertTime := now.Format(\"2006-01-02 15:04:05\")\naerr := ztm.SetTaskAlert(editedTask.Id, alertTime)\n\n...\n\n// move the Task to Do\n\nztm.MoveTaskToRealm(editedTask.Id, \"2\")\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"2\"}\nzom.AddPath(taskPath)\n\n// after the task is done, we sent it back to Assess\n\nztm.MoveTaskToRealm(editedTask.Id,\"1\")\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"1\"}\nzom.AddPath(taskPath)\n\n// from here, we can add it to a collection\n\nmyCollection := zcm.GetCollectionById(\"1\")\n\nzcm.AddTaskToCollection(ztm, myCollection, editedTask)\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"4\"}\nzom.AddPath(taskPath)\n\n```\n\nAll tests are in the `*_test.gno` files, e.g. `tasks_test.gno`, `projects_test.gno`, etc."
+ },
+ {
+ "name": "collections.gno",
+ "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\n\ntype Collection struct {\n\tId \t\t\tstring `json:\"collectionId\"`\n\tRealmId \tstring `json:\"collectionRealmId\"`\n\tName \t\tstring `json:\"collectionName\"`\n\tTasks\t\t[]Task `json:\"collectionTasks\"`\n\tProjects\t[]Project `json:\"collectionProjects\"`\n}\n\ntype ZCollectionManager struct {\n\tCollections *avl.Tree \n\tCollectionTasks *avl.Tree\n\tCollectionProjects *avl.Tree \n}\n\nfunc NewZCollectionManager() *ZCollectionManager {\n return &ZCollectionManager{\n Collections: avl.NewTree(),\n CollectionTasks: avl.NewTree(),\n CollectionProjects: avl.NewTree(),\n }\n}\n\n\n// actions\n\nfunc (zcolm *ZCollectionManager) AddCollection(c Collection) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif exist {\n\t\t\treturn ErrCollectionIdAlreadyExists\n\t\t}\n\t}\n\tzcolm.Collections.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) EditCollection(c Collection) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\t\n\tzcolm.Collections.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) RemoveCollection(c Collection) (err error) {\n // implementation\n if zcolm.Collections.Size() != 0 {\n collectionInterface, exist := zcolm.Collections.Get(c.Id)\n if !exist {\n return ErrCollectionIdNotFound\n }\n collection := collectionInterface.(Collection)\n\n _, removed := zcolm.Collections.Remove(collection.Id)\n if !removed {\n return ErrCollectionNotRemoved\n }\n\n if zcolm.CollectionTasks.Size() != 0 {\n _, removedTasks := zcolm.CollectionTasks.Remove(collection.Id)\n if !removedTasks {\n return ErrCollectionNotRemoved\n }\t\n }\n\n if zcolm.CollectionProjects.Size() != 0 {\n _, removedProjects := zcolm.CollectionProjects.Remove(collection.Id)\n if !removedProjects {\n return ErrCollectionNotRemoved\n }\t\n }\n }\n return nil\n}\n\n\nfunc (zcolm *ZCollectionManager) AddProjectToCollection(zpm *ZProjectManager, c Collection, p Project) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionProjects, texist := zcolm.CollectionProjects.Get(c.Id)\n\tif !texist {\n\t\t// If the collections has no projects yet, initialize the slice.\n\t\texistingCollectionProjects = []Project{}\n\t} else {\n\t\tprojects, ok := existingCollectionProjects.([]Project)\n\t\tif !ok {\n\t\t\treturn ErrCollectionsProjectsNotFound\n\t\t}\n\t\texistingCollectionProjects = projects\n\t}\n\tp.RealmId = \"4\"\n\tif err := zpm.EditProject(p); err != nil {\n\t\treturn err\n\t}\n\tupdatedProjects := append(existingCollectionProjects.([]Project), p)\n\tzcolm.CollectionProjects.Set(c.Id, updatedProjects)\n\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) AddTaskToCollection(ztm *ZTaskManager, c Collection, t Task) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionTasks, texist := zcolm.CollectionTasks.Get(c.Id)\n\tif !texist {\n\t\t// If the collections has no tasks yet, initialize the slice.\n\t\texistingCollectionTasks = []Task{}\n\t} else {\n\t\ttasks, ok := existingCollectionTasks.([]Task)\n\t\tif !ok {\n\t\t\treturn ErrCollectionsTasksNotFound\n\t\t}\n\t\texistingCollectionTasks = tasks\n\t}\n\tt.RealmId = \"4\"\n\tif err := ztm.EditTask(t); err != nil {\n\t\treturn err\n\t}\n\tupdatedTasks := append(existingCollectionTasks.([]Task), t)\n\tzcolm.CollectionTasks.Set(c.Id, updatedTasks)\n\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) RemoveProjectFromCollection(zpm *ZProjectManager, c Collection, p Project) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionProjects, texist := zcolm.CollectionProjects.Get(c.Id)\n\tif !texist {\n\t\t// If the collection has no projects yet, return appropriate error\n\t\treturn ErrCollectionsProjectsNotFound\n\t}\n\n\t// Find the index of the project to be removed.\n\tvar index int = -1\n\tfor i, project := range existingCollectionProjects.([]Project) {\n\t\tif project.Id == p.Id {\n\t\t\tindex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// If the project was found, we remove it from the slice.\n\tif index != -1 {\n\t\t// by default we send it back to Assess\n\t\tp.RealmId = \"1\"\n\t\tzpm.EditProject(p)\n\t\texistingCollectionProjects = append(existingCollectionProjects.([]Project)[:index], existingCollectionProjects.([]Project)[index+1:]...)\n\t} else {\n\t\t// Project not found in the collection\n\t\treturn ErrProjectByIdNotFound \n\t}\n\tzcolm.CollectionProjects.Set(c.Id, existingCollectionProjects)\n\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) RemoveTaskFromCollection(ztm *ZTaskManager, c Collection, t Task) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionTasks, texist := zcolm.CollectionTasks.Get(c.Id)\n\tif !texist {\n\t\t// If the collection has no tasks yet, return appropriate error\n\t\treturn ErrCollectionsTasksNotFound\n\t}\n\n\t// Find the index of the task to be removed.\n\tvar index int = -1\n\tfor i, task := range existingCollectionTasks.([]Task) {\n\t\tif task.Id == t.Id {\n\t\t\tindex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// If the task was found, we remove it from the slice.\n\tif index != -1 {\n\t\t// by default, we send the task to Assess\n\t\tt.RealmId = \"1\"\n\t\tztm.EditTask(t)\n\t\texistingCollectionTasks = append(existingCollectionTasks.([]Task)[:index], existingCollectionTasks.([]Task)[index+1:]...)\n\t} else {\n\t\t// Task not found in the collection\n\t\treturn ErrTaskByIdNotFound \n\t}\n\tzcolm.CollectionTasks.Set(c.Id, existingCollectionTasks)\n\n\treturn nil\n}\n\n// getters\n\nfunc (zcolm *ZCollectionManager) GetCollectionById(collectionId string) (Collection, error) {\n if zcolm.Collections.Size() != 0 {\n cInterface, exist := zcolm.Collections.Get(collectionId)\n if exist {\n collection := cInterface.(Collection)\n // look for collection Tasks, Projects\n existingCollectionTasks, texist := zcolm.CollectionTasks.Get(collectionId)\n if texist {\n collection.Tasks = existingCollectionTasks.([]Task)\n }\n existingCollectionProjects, pexist := zcolm.CollectionProjects.Get(collectionId)\n if pexist {\n collection.Projects = existingCollectionProjects.([]Project)\n }\n return collection, nil\n }\n return Collection{}, ErrCollectionByIdNotFound\n }\n return Collection{}, ErrCollectionByIdNotFound\n}\n\nfunc (zcolm *ZCollectionManager) GetCollectionTasks(c Collection) (tasks []Task, err error) {\n\t\n\tif zcolm.CollectionTasks.Size() != 0 {\n\t\ttask, exist := zcolm.CollectionTasks.Get(c.Id)\n\t\tif !exist {\n\t\t\t// if there's no record in CollectionTasks, we don't have to return anything\n\t\t\treturn nil, ErrCollectionsTasksNotFound\n\t\t} else {\n\t\t\t// type assertion to convert interface{} to []Task\n\t\t\texistingCollectionTasks, ok := task.([]Task)\n\t\t\tif !ok {\n\t\t\t\treturn nil, ErrTaskFailedToAssert\n\t\t\t}\n\t\t\treturn existingCollectionTasks, nil\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc (zcolm *ZCollectionManager) GetCollectionProjects(c Collection) (projects []Project, err error) {\n\t\n\tif zcolm.CollectionProjects.Size() != 0 {\n\t\tproject, exist := zcolm.CollectionProjects.Get(c.Id)\n\t\tif !exist {\n\t\t\t// if there's no record in CollectionProjets, we don't have to return anything\n\t\t\treturn nil, ErrCollectionsProjectsNotFound\n\t\t} else {\n\t\t\t// type assertion to convert interface{} to []Projet\n\t\t\texistingCollectionProjects, ok := project.([]Project)\n\t\t\tif !ok {\n\t\t\t\treturn nil, ErrProjectFailedToAssert\n\t\t\t}\n\t\t\treturn existingCollectionProjects, nil\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc (zcolm *ZCollectionManager) GetAllCollections() (collections string, err error) {\n\t// implementation\n\tvar allCollections []Collection\n\t\n\t// Iterate over the Collections AVL tree to collect all Project objects.\n\t\n\tzcolm.Collections.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif collection, ok := value.(Collection); ok {\n\t\t\t// get collection tasks, if any\n\t\t\tcollectionTasks, _ := zcolm.GetCollectionTasks(collection)\n\t\t\tif collectionTasks != nil {\n\t\t\t\tcollection.Tasks = collectionTasks\n\t\t\t}\n\t\t\t// get collection prokects, if any\n\t\t\tcollectionProjects, _ := zcolm.GetCollectionProjects(collection)\n\t\t\tif collectionProjects != nil {\n\t\t\t\tcollection.Projects = collectionProjects\n\t\t\t}\n\t\t\tallCollections = append(allCollections, collection)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a CollectionsObject with all collected tasks.\n\tcollectionsObject := CollectionsObject{\n\t\tCollections: allCollections,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the collections into JSON.\n\tmarshalledCollections, merr := collectionsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\", merr\n\t} \n\treturn string(marshalledCollections), nil\n} "
+ },
+ {
+ "name": "collections_test.gno",
+ "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n/*\n\nfunc Test_AddCollection(t *testing.T) {\n \n collection := Collection{Id: \"1\", RealmId: \"4\", Name: \"First collection\",}\n\n // Test adding a collection successfully.\n err := collection.AddCollection()\n if err != nil {\n t.Errorf(\"Failed to add collection: %v\", err)\n }\n\n // Test adding a duplicate task.\n cerr := collection.AddCollection()\n if cerr != ErrCollectionIdAlreadyExists {\n t.Errorf(\"Expected ErrCollectionIdAlreadyExists, got %v\", cerr)\n }\n}\n\nfunc Test_RemoveCollection(t *testing.T) {\n \n collection := Collection{Id: \"20\", RealmId: \"4\", Name: \"Removable collection\",}\n\n // Test adding a collection successfully.\n err := collection.AddCollection()\n if err != nil {\n t.Errorf(\"Failed to add collection: %v\", err)\n }\n\n retrievedCollection, rerr := GetCollectionById(collection.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added collection\")\n }\n\n // Test removing a collection\n terr := retrievedCollection.RemoveCollection()\n if terr != ErrCollectionNotRemoved {\n t.Errorf(\"Expected ErrCollectionNotRemoved, got %v\", terr)\n }\n}\n\nfunc Test_EditCollection(t *testing.T) {\n \n collection := Collection{Id: \"2\", RealmId: \"4\", Name: \"Second collection\",}\n\n // Test adding a collection successfully.\n err := collection.AddCollection()\n if err != nil {\n t.Errorf(\"Failed to add collection: %v\", err)\n }\n\n // Test editing the collection\n editedCollection := Collection{Id: collection.Id, RealmId: collection.RealmId, Name: \"Edited collection\",}\n cerr := editedCollection.EditCollection()\n if cerr != nil {\n t.Errorf(\"Failed to edit the collection\")\n }\n\n retrievedCollection, _ := GetCollectionById(editedCollection.Id)\n if retrievedCollection.Name != \"Edited collection\" {\n t.Errorf(\"Collection was not edited\")\n }\n}\n\nfunc Test_AddProjectToCollection(t *testing.T){\n // Example Collection and Projects\n col := Collection{Id: \"1\", Name: \"First collection\", RealmId: \"4\",}\n prj := Project{Id: \"10\", Body: \"Project 10\", RealmId: \"1\",}\n\n Collections.Set(col.Id, col) // Mock existing collections\n\n tests := []struct {\n name string\n collection Collection\n project Project\n wantErr bool\n errMsg error\n }{\n {\n name: \"Attach to existing collection\",\n collection: col,\n project: prj,\n wantErr: false,\n },\n {\n name: \"Attach to non-existing collection\",\n collection: Collection{Id: \"200\", Name: \"Collection 200\", RealmId: \"4\",},\n project: prj,\n wantErr: true,\n errMsg: ErrCollectionIdNotFound,\n },\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n err := tt.collection.AddProjectToCollection(tt.project)\n if (err != nil) != tt.wantErr {\n t.Errorf(\"AddProjectToCollection() error = %v, wantErr %v\", err, tt.wantErr)\n }\n if tt.wantErr && err != tt.errMsg {\n t.Errorf(\"AddProjectToCollection() error = %v, expected %v\", err, tt.errMsg)\n }\n\n // For successful attach, verify the project is added to the collection's tasks.\n if !tt.wantErr {\n projects, exist := CollectionProjects.Get(tt.collection.Id)\n if !exist || len(projects.([]Project)) == 0 {\n t.Errorf(\"Project was not added to the collection\")\n } else {\n found := false\n for _, project := range projects.([]Project) {\n if project.Id == tt.project.Id {\n found = true\n break\n }\n }\n if !found {\n t.Errorf(\"Project was not attached to the collection\")\n }\n }\n }\n })\n }\n}\n\nfunc Test_AddTaskToCollection(t *testing.T){\n // Example Collection and Tasks\n col := Collection{Id: \"2\", Name: \"Second Collection\", RealmId: \"4\",}\n tsk := Task{Id: \"30\", Body: \"Task 30\", RealmId: \"1\",}\n\n Collections.Set(col.Id, col) // Mock existing collections\n\n tests := []struct {\n name string\n collection Collection\n task Task\n wantErr bool\n errMsg error\n }{\n {\n name: \"Attach to existing collection\",\n collection: col,\n task: tsk,\n wantErr: false,\n },\n {\n name: \"Attach to non-existing collection\",\n collection: Collection{Id: \"210\", Name: \"Collection 210\", RealmId: \"4\",},\n task: tsk,\n wantErr: true,\n errMsg: ErrCollectionIdNotFound,\n },\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n err := tt.collection.AddTaskToCollection(tt.task)\n if (err != nil) != tt.wantErr {\n t.Errorf(\"AddTaskToCollection() error = %v, wantErr %v\", err, tt.wantErr)\n }\n if tt.wantErr && err != tt.errMsg {\n t.Errorf(\"AddTaskToCollection() error = %v, expected %v\", err, tt.errMsg)\n }\n\n // For successful attach, verify the task is added to the collection's tasks.\n if !tt.wantErr {\n tasks, exist := CollectionTasks.Get(tt.collection.Id)\n if !exist || len(tasks.([]Task)) == 0 {\n t.Errorf(\"Task was not added to the collection\")\n } else {\n found := false\n for _, task := range tasks.([]Task) {\n if task.Id == tt.task.Id {\n found = true\n break\n }\n }\n if !found {\n t.Errorf(\"Task was not attached to the collection\")\n }\n }\n }\n })\n }\n}\n\nfunc Test_RemoveProjectFromCollection(t *testing.T){\n // Setup:\n\tcollection := Collection{Id: \"300\", Name: \"Collection 300\",}\n\tproject1 := Project{Id: \"21\", Body: \"Project 21\", RealmId: \"1\",}\n\tproject2 := Project{Id: \"22\", Body: \"Project 22\", RealmId: \"1\",}\n\n collection.AddCollection()\n project1.AddProject()\n project2.AddProject()\n collection.AddProjectToCollection(project1)\n collection.AddProjectToCollection(project2)\n\n\ttests := []struct {\n\t\tname string\n\t\tproject Project\n\t\tcollection Collection\n\t\twantErr bool\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"Remove existing project from collection\",\n\t\t\tproject: project1,\n\t\t\tcollection: collection,\n\t\t\twantErr: false,\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove project from non-existing collection\",\n\t\t\tproject: project1,\n\t\t\tcollection: Collection{Id: \"nonexistent\"},\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrCollectionIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove non-existing project from collection\",\n\t\t\tproject: Project{Id: \"nonexistent\"},\n\t\t\tcollection: collection,\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrProjectByIdNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.collection.RemoveProjectFromCollection(tt.project)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil || err != tt.expectedErr {\n\t\t\t\t\tt.Errorf(\"%s: expected error %v, got %v\", tt.name, tt.expectedErr, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"%s: unexpected error: %v\", tt.name, err)\n\t\t\t\t}\n\n\t\t\t\t// For successful removal, verify the project is no longer part of the collection's projects\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\tprojects, _ := CollectionProjects.Get(tt.collection.Id)\n\t\t\t\t\tfor _, project := range projects.([]Project) {\n\t\t\t\t\t\tif project.Id == tt.project.Id {\n\t\t\t\t\t\t\tt.Errorf(\"%s: project was not detached from the collection\", tt.name)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_RemoveTaskFromCollection(t *testing.T){\n // setup, re-using parts from Test_AddTaskToCollection\n\tcollection := Collection{Id: \"40\", Name: \"Collection 40\",}\n task1 := Task{Id: \"40\", Body: \"Task 40\", RealmId: \"1\",}\n\n collection.AddCollection()\n task1.AddTask()\n collection.AddTaskToCollection(task1)\n\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\tcollection Collection\n\t\twantErr bool\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"Remove existing task from collection\",\n\t\t\ttask: task1,\n\t\t\tcollection: collection,\n\t\t\twantErr: false,\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove task from non-existing collection\",\n\t\t\ttask: task1,\n\t\t\tcollection: Collection{Id: \"nonexistent\"},\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrCollectionIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove non-existing task from collection\",\n\t\t\ttask: Task{Id: \"nonexistent\"},\n\t\t\tcollection: collection,\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrTaskByIdNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.collection.RemoveTaskFromCollection(tt.task)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil || err != tt.expectedErr {\n\t\t\t\t\tt.Errorf(\"%s: expected error %v, got %v\", tt.name, tt.expectedErr, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"%s: unexpected error: %v\", tt.name, err)\n\t\t\t\t}\n\n\t\t\t\t// For successful removal, verify the task is no longer part of the collection's tasks\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\ttasks, _ := CollectionTasks.Get(tt.collection.Id)\n\t\t\t\t\tfor _, task := range tasks.([]Task) {\n\t\t\t\t\t\tif task.Id == tt.task.Id {\n\t\t\t\t\t\t\tt.Errorf(\"%s: task was not detached from the collection\", tt.name)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_GetCollectionById(t *testing.T){\n // test getting a non-existing collection\n nonCollection, err := GetCollectionById(\"0\")\n if err != ErrCollectionByIdNotFound {\n t.Fatalf(\"Expected ErrCollectionByIdNotFound, got: %v\", err)\n }\n\n // test getting the correct collection by id\n correctCollection, err := GetCollectionById(\"1\")\n if err != nil {\n t.Fatalf(\"Failed to get collection by id, error: %v\", err)\n }\n\n if correctCollection.Name != \"First collection\" {\n t.Fatalf(\"Got the wrong collection, with name: %v\", correctCollection.Name)\n }\n}\n\nfunc Test_GetCollectionTasks(t *testing.T) {\n // retrieving objects based on these mocks\n //col := Collection{Id: \"2\", Name: \"Second Collection\", RealmId: \"4\",}\n tsk := Task{Id: \"30\", Body: \"Task 30\", RealmId: \"1\",}\n\n collection, cerr := GetCollectionById(\"2\")\n if cerr != nil {\n t.Errorf(\"GetCollectionById() failed, %v\", cerr)\n }\n\n collectionTasks, pterr := collection.GetCollectionTasks()\n if len(collectionTasks) == 0 {\n t.Errorf(\"GetCollectionTasks() failed, %v\", pterr)\n }\n\n // test detaching from an existing collection\n dtterr := collection.RemoveTaskFromCollection(tsk)\n if dtterr != nil {\n t.Errorf(\"RemoveTaskFromCollection() failed, %v\", dtterr)\n }\n\n collectionWithNoTasks, pterr := collection.GetCollectionTasks()\n if len(collectionWithNoTasks) != 0 {\n t.Errorf(\"GetCollectionTasks() after detach failed, %v\", pterr)\n }\n\n // add task back to collection, for tests mockup integrity\n collection.AddTaskToCollection(tsk)\n}\n\nfunc Test_GetCollectionProjects(t *testing.T) {\n // retrieving objects based on these mocks\n //col := Collection{Id: \"1\", Name: \"First Collection\", RealmId: \"4\",}\n prj := Project{Id: \"10\", Body: \"Project 10\", RealmId: \"2\", ContextId: \"2\", Due: \"2024-01-01\"}\n\n collection, cerr := GetCollectionById(\"1\")\n if cerr != nil {\n t.Errorf(\"GetCollectionById() failed, %v\", cerr)\n }\n\n collectionProjects, pterr := collection.GetCollectionProjects()\n if len(collectionProjects) == 0 {\n t.Errorf(\"GetCollectionProjects() failed, %v\", pterr)\n }\n\n // test detaching from an existing collection\n dtterr := collection.RemoveProjectFromCollection(prj)\n if dtterr != nil {\n t.Errorf(\"RemoveProjectFromCollection() failed, %v\", dtterr)\n }\n\n collectionWithNoProjects, pterr := collection.GetCollectionProjects()\n if len(collectionWithNoProjects) != 0 {\n t.Errorf(\"GetCollectionProjects() after detach failed, %v\", pterr)\n }\n\n // add project back to collection, for tests mockup integrity\n collection.AddProjectToCollection(prj)\n}\n\nfunc Test_GetAllCollections(t *testing.T){\n // mocking the collections based on previous tests\n // TODO: add isolation?\n knownCollections := []Collection{\n {\n Id: \"1\",\n RealmId: \"4\",\n Name: \"First collection\",\n Tasks: nil, \n Projects: []Project{\n {\n Id: \"10\",\n ContextId: \"2\",\n RealmId: \"4\",\n Tasks: nil, \n Body: \"Project 10\",\n Due: \"2024-01-01\",\n },\n },\n },\n {\n Id: \"2\",\n RealmId: \"4\",\n Name: \"Second Collection\",\n Tasks: []Task{\n {\n Id:\"30\",\n ProjectId:\"\",\n ContextId:\"\",\n RealmId:\"4\",\n Body:\"Task 30\",\n Due:\"\",\n Alert:\"\",\n },\n },\n Projects: nil, \n },\n {\n Id:\"20\",\n RealmId:\"4\",\n Name:\"Removable collection\",\n Tasks: nil,\n Projects: nil,\n },\n {\n Id: \"300\",\n Name: \"Collection 300\",\n Tasks: nil, \n Projects: []Project {\n {\n Id:\"22\",\n ContextId:\"\",\n RealmId:\"4\",\n Tasks: nil,\n Body:\"Project 22\",\n Due:\"\",\n },\n }, \n },\n {\n Id: \"40\",\n Name: \"Collection 40\",\n Tasks: nil, \n Projects: nil, \n },\n }\n \n\n // Manually marshal the known collections to create the expected outcome.\n collectionsObject := CollectionsObject{Collections: knownCollections}\n expected, err := collectionsObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known collections: %v\", err)\n }\n\n // Execute GetAllCollections() to get the actual outcome.\n actual, err := GetAllCollections()\n if err != nil {\n t.Fatalf(\"GetAllCollections() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual collections JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n\n\n\n\n"
+ },
+ {
+ "name": "contexts.gno",
+ "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\ntype Context struct {\n\tId string `json:\"contextId\"`\n\tName string `json:\"contextName\"`\n}\n\ntype ZContextManager struct {\n\tContexts *avl.Tree\n}\n\nfunc NewZContextManager() *ZContextManager {\n\treturn &ZContextManager{\n\t\tContexts: avl.NewTree(),\n\t}\n}\n\n// Actions\n\nfunc (zcm *ZContextManager) AddContext(c Context) error {\n\tif zcm.Contexts.Size() != 0 {\n\t\t_, exist := zcm.Contexts.Get(c.Id)\n\t\tif exist {\n\t\t\treturn ErrContextIdAlreadyExists\n\t\t}\n\t}\n\tzcm.Contexts.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) EditContext(c Context) error {\n\tif zcm.Contexts.Size() != 0 {\n\t\t_, exist := zcm.Contexts.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrContextIdNotFound\n\t\t}\n\t}\n\tzcm.Contexts.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) RemoveContext(c Context) error {\n\tif zcm.Contexts.Size() != 0 {\n\t\tcontext, exist := zcm.Contexts.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrContextIdNotFound\n\t\t}\n\t\t_, removed := zcm.Contexts.Remove(context.(Context).Id)\n\t\tif !removed {\n\t\t\treturn ErrContextNotRemoved\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) AddContextToTask(ztm *ZTaskManager, c Context, t Task) error {\n\ttaskInterface, exist := ztm.Tasks.Get(t.Id)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\t_, cexist := zcm.Contexts.Get(c.Id)\n\tif !cexist {\n\t\treturn ErrContextIdNotFound\n\t}\n\n\tif t.RealmId == \"2\" {\n\t\ttask := taskInterface.(Task)\n\t\ttask.ContextId = c.Id\n\t\tztm.Tasks.Set(t.Id, task)\n\t} else {\n\t\treturn ErrTaskNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) AddContextToProject(zpm *ZProjectManager, c Context, p Project) error {\n\tprojectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\t_, cexist := zcm.Contexts.Get(c.Id)\n\tif !cexist {\n\t\treturn ErrContextIdNotFound\n\t}\n\n\tif p.RealmId == \"2\" {\n\t\tproject := projectInterface.(Project)\n\t\tproject.ContextId = c.Id\n\t\tzpm.Projects.Set(p.Id, project)\n\t} else {\n\t\treturn ErrProjectNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) AddContextToProjectTask(zpm *ZProjectManager, c Context, p Project, projectTaskId string) error {\n\t\n\t_, cexist := zcm.Contexts.Get(c.Id)\n\tif !cexist {\n\t\treturn ErrContextIdNotFound\n\t}\n\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n\tif existingProject.RealmId != \"2\" {\n\t\treturn ErrProjectNotEditable\n\t}\n\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(p.Id)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n existingProject.Tasks = tasks\n\n var index int = -1\n for i, task := range existingProject.Tasks {\n if task.Id == projectTaskId {\n index = i\n break\n }\n }\n\n if index != -1 {\n existingProject.Tasks[index].ContextId = c.Id\n } else {\n return ErrTaskByIdNotFound\n }\n\n zpm.ProjectTasks.Set(p.Id, existingProject.Tasks)\n return nil\n}\n\n// getters\n\nfunc (zcm *ZContextManager) GetContextById(contextId string) (Context, error) {\n\tif zcm.Contexts.Size() != 0 {\n\t\tcInterface, exist := zcm.Contexts.Get(contextId)\n\t\tif exist {\n\t\t\treturn cInterface.(Context), nil\n\t\t}\n\t\treturn Context{}, ErrContextIdNotFound\n\t}\n\treturn Context{}, ErrContextIdNotFound\n}\n\nfunc (zcm *ZContextManager) GetAllContexts() (string) {\n\tvar allContexts []Context\n\n\t// Iterate over the Contexts AVL tree to collect all Context objects.\n\tzcm.Contexts.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif context, ok := value.(Context); ok {\n\t\t\tallContexts = append(allContexts, context)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ContextsObject with all collected contexts.\n\tcontextsObject := &ContextsObject{\n\t\tContexts: allContexts,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the contexts into JSON.\n\tmarshalledContexts, merr := contextsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t}\n\treturn string(marshalledContexts)\n}\n\n"
+ },
+ {
+ "name": "contexts_test.gno",
+ "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n/*\nfunc Test_AddContext(t *testing.T) {\n \n context := Context{Id: \"1\", Name: \"Work\"}\n\n // Test adding a context successfully.\n err := context.AddContext()\n if err != nil {\n t.Errorf(\"Failed to add context: %v\", err)\n }\n\n // Test adding a duplicate task.\n cerr := context.AddContext()\n if cerr != ErrContextIdAlreadyExists {\n t.Errorf(\"Expected ErrContextIdAlreadyExists, got %v\", cerr)\n }\n}\n\nfunc Test_EditContext(t *testing.T) {\n \n context := Context{Id: \"2\", Name: \"Home\"}\n\n // Test adding a context successfully.\n err := context.AddContext()\n if err != nil {\n t.Errorf(\"Failed to add context: %v\", err)\n }\n\n // Test editing the context\n editedContext := Context{Id: \"2\", Name: \"Shopping\"}\n cerr := editedContext.EditContext()\n if cerr != nil {\n t.Errorf(\"Failed to edit the context\")\n }\n\n retrievedContext, _ := GetContextById(editedContext.Id)\n if retrievedContext.Name != \"Shopping\" {\n t.Errorf(\"Context was not edited\")\n }\n}\n\nfunc Test_RemoveContext(t *testing.T) {\n \n context := Context{Id: \"4\", Name: \"Gym\",}\n\n // Test adding a context successfully.\n err := context.AddContext()\n if err != nil {\n t.Errorf(\"Failed to add context: %v\", err)\n }\n\n retrievedContext, rerr := GetContextById(context.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added context\")\n }\n // Test removing a context\n cerr := retrievedContext.RemoveContext()\n if cerr != ErrContextNotRemoved {\n t.Errorf(\"Expected ErrContextNotRemoved, got %v\", cerr)\n }\n}\n\nfunc Test_AddContextToTask(t *testing.T) {\n\n task := Task{Id: \"10\", Body: \"First content\", RealmId: \"2\", ContextId: \"1\",}\n\n // Test adding a task successfully.\n err := task.AddTask()\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n taskInDecide, exist := Tasks.Get(\"10\")\n\tif !exist {\n\t\tt.Errorf(\"Task with id 10 not found\")\n\t}\n\t// check if context exists\n\tcontextToAdd, cexist := Contexts.Get(\"2\")\n\tif !cexist {\n\t\tt.Errorf(\"Context with id 2 not found\")\n\t}\n\n derr := contextToAdd.(Context).AddContextToTask(taskInDecide.(Task))\n if derr != nil {\n t.Errorf(\"Could not add context to a task in Decide, err %v\", derr)\n }\n}\n\nfunc Test_AddContextToProject(t *testing.T) {\n\n project := Project{Id: \"10\", Body: \"Project 10\", RealmId: \"2\", ContextId: \"1\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n projectInDecide, exist := Projects.Get(\"10\")\n\tif !exist {\n\t\tt.Errorf(\"Project with id 10 not found\")\n\t}\n\t// check if context exists\n\tcontextToAdd, cexist := Contexts.Get(\"2\")\n\tif !cexist {\n\t\tt.Errorf(\"Context with id 2 not found\")\n\t}\n\n derr := contextToAdd.(Context).AddContextToProject(projectInDecide.(Project))\n if derr != nil {\n t.Errorf(\"Could not add context to a project in Decide, err %v\", derr)\n }\n}\n\nfunc Test_GetAllContexts(t *testing.T) {\n \n // mocking the contexts based on previous tests\n // TODO: add isolation?\n knownContexts := []Context{\n {Id: \"1\", Name: \"Work\",},\n {Id: \"2\", Name: \"Shopping\",},\n {Id: \"4\", Name: \"Gym\",},\n }\n\n // Manually marshal the known contexts to create the expected outcome.\n contextsObject := ContextsObject{Contexts: knownContexts}\n expected, err := contextsObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known contexts: %v\", err)\n }\n\n // Execute GetAllContexts() to get the actual outcome.\n actual, err := GetAllContexts()\n if err != nil {\n t.Fatalf(\"GetAllContexts() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual contexts JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n\n"
+ },
+ {
+ "name": "core.gno",
+ "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n// holding the path of an object since creation\n// each time we move an object from one realm to another, we add to its path\ntype ObjectPath struct {\n\tObjectType string `json:\"objectType\"` // Task, Project\n\tId string `json:\"id\"` // this is the Id of the object moved, Task, Project\n\tRealmId string `json:\"realmId\"`\n}\n\ntype ZObjectPathManager struct {\n\tPaths avl.Tree\n\tPathId int\n}\n\nfunc NewZObjectPathManager() *ZObjectPathManager {\n\treturn &ZObjectPathManager{\n\t\tPaths: *avl.NewTree(),\n\t\tPathId: 1,\n\t}\n}\n\nfunc (zopm *ZObjectPathManager) AddPath(o ObjectPath) error {\n\tzopm.PathId++\n\tupdated := zopm.Paths.Set(strconv.Itoa(zopm.PathId), o)\n\tif !updated {\n\t\treturn ErrObjectPathNotUpdated\n\t}\n\treturn nil\n}\n\nfunc (zopm *ZObjectPathManager) GetObjectJourney(objectType string, objectId string) (string, error) {\n\tvar objectPaths []ObjectPath\n\n\t// Iterate over the Paths AVL tree to collect all ObjectPath objects.\n\tzopm.Paths.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif objectPath, ok := value.(ObjectPath); ok {\n\t\t\tif objectPath.ObjectType == objectType && objectPath.Id == objectId {\n\t\t\t\tobjectPaths = append(objectPaths, objectPath)\n\t\t\t}\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create an ObjectJourney with all collected paths.\n\tobjectJourney := &ObjectJourney{\n\t\tObjectPaths: objectPaths,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the journey into JSON.\n\tmarshalledJourney, merr := objectJourney.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\", merr\n\t}\n\treturn string(marshalledJourney), nil\n}\n\n\n// GetZenStatus\n/* todo: leave it to the client\nfunc () GetZenStatus() (zenStatus string, err error) {\n\t// implementation\n}\n*/\n"
+ },
+ {
+ "name": "errors.gno",
+ "body": "package zentasktic\n\nimport \"errors\"\n\nvar (\n\tErrTaskNotEditable \t= errors.New(\"Task is not editable\")\n\tErrProjectNotEditable = errors.New(\"Project is not editable\")\n\tErrProjectIdNotFound\t\t\t= errors.New(\"Project id not found\")\n\tErrTaskIdNotFound\t\t\t\t= errors.New(\"Task id not found\")\n\tErrTaskFailedToAssert\t\t\t= errors.New(\"Failed to assert Task type\")\n\tErrProjectFailedToAssert\t\t= errors.New(\"Failed to assert Project type\")\n\tErrProjectTasksNotFound\t\t\t= errors.New(\"Could not get tasks for project\")\n\tErrCollectionsProjectsNotFound\t= errors.New(\"Could not get projects for this collection\")\n\tErrCollectionsTasksNotFound\t\t= errors.New(\"Could not get tasks for this collection\")\n\tErrTaskIdAlreadyExists\t\t\t= errors.New(\"A task with the provided id already exists\")\n\tErrCollectionIdAlreadyExists\t= errors.New(\"A collection with the provided id already exists\")\n\tErrProjectIdAlreadyExists\t\t= errors.New(\"A project with the provided id already exists\")\n\tErrTaskByIdNotFound\t\t\t\t= errors.New(\"Can't get task by id\")\n\tErrProjectByIdNotFound\t\t\t= errors.New(\"Can't get project by id\")\n\tErrCollectionByIdNotFound\t\t= errors.New(\"Can't get collection by id\")\n\tErrTaskNotRemovable\t\t\t\t= errors.New(\"Cannot remove a task directly from this realm\")\n\tErrProjectNotRemovable\t\t\t= errors.New(\"Cannot remove a project directly from this realm\")\n\tErrProjectTasksNotRemoved\t\t= errors.New(\"Project tasks were not removed\")\n\tErrTaskNotRemoved\t\t\t\t= errors.New(\"Task was not removed\")\n\tErrTaskNotInAssessRealm\t\t\t= errors.New(\"Task is not in Assess, cannot edit Body\")\n\tErrProjectNotInAssessRealm\t\t= errors.New(\"Project is not in Assess, cannot edit Body\")\n\tErrContextIdAlreadyExists\t\t= errors.New(\"A context with the provided id already exists\")\n\tErrContextIdNotFound\t\t\t= errors.New(\"Context id not found\")\n\tErrCollectionIdNotFound\t\t\t= errors.New(\"Collection id not found\")\n\tErrContextNotRemoved\t\t\t= errors.New(\"Context was not removed\")\n\tErrProjectNotRemoved\t\t\t= errors.New(\"Project was not removed\")\n\tErrCollectionNotRemoved\t\t\t= errors.New(\"Collection was not removed\")\n\tErrObjectPathNotUpdated\t\t\t= errors.New(\"Object path wasn't updated\")\n\tErrInvalidateDateFormat\t\t\t= errors.New(\"Invalida date format\")\n\tErrInvalidDateFilterType\t\t= errors.New(\"Invalid date filter type\")\n\tErrRealmIdAlreadyExists\t\t\t= errors.New(\"A realm with the same id already exists\")\n\tErrRealmIdNotAllowed\t\t\t= errors.New(\"This is a reserved realm id\")\n\tErrRealmIdNotFound\t\t\t\t= errors.New(\"Realm id not found\")\n\tErrRealmNotRemoved\t\t\t\t= errors.New(\"Realm was not removed\")\n)"
+ },
+ {
+ "name": "marshals.gno",
+ "body": "package zentasktic\n\nimport (\n\t\"bytes\"\n)\n\n\ntype ContextsObject struct {\n\tContexts\t[]Context\n}\n\ntype TasksObject struct {\n\tTasks\t[]Task\n}\n\ntype ProjectsObject struct {\n\tProjects\t[]Project\n}\n\ntype CollectionsObject struct {\n\tCollections\t[]Collection\n}\n\ntype RealmsObject struct {\n\tRealms\t[]Realm\n}\n\ntype ObjectJourney struct {\n\tObjectPaths []ObjectPath\n}\n\nfunc (c Context) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"contextId\":\"`)\n\tb.WriteString(c.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"contextName\":\"`)\n\tb.WriteString(c.Name)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (cs ContextsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"contexts\":[`)\n\t\n\tfor i, context := range cs.Contexts {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcontextJSON, cerr := context.MarshalJSON()\n\t\tif cerr == nil {\n\t\t\tb.WriteString(string(contextJSON))\n\t\t}\n\t}\n\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (t Task) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"taskId\":\"`)\n\tb.WriteString(t.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskProjectId\":\"`)\n\tb.WriteString(t.ProjectId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskContextId\":\"`)\n\tb.WriteString(t.ContextId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskRealmId\":\"`)\n\tb.WriteString(t.RealmId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskBody\":\"`)\n\tb.WriteString(t.Body)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskDue\":\"`)\n\tb.WriteString(t.Due)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskAlert\":\"`)\n\tb.WriteString(t.Alert)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (ts TasksObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"tasks\":[`)\n\tfor i, task := range ts.Tasks {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\ttaskJSON, cerr := task.MarshalJSON()\n\t\tif cerr == nil {\n\t\t\tb.WriteString(string(taskJSON))\n\t\t}\n\t}\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (p Project) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"projectId\":\"`)\n\tb.WriteString(p.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectContextId\":\"`)\n\tb.WriteString(p.ContextId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectRealmId\":\"`)\n\tb.WriteString(p.RealmId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectTasks\":[`)\n\n\tif len(p.Tasks) != 0 {\n\t\tfor i, projectTask := range p.Tasks {\n\t\t\tif i > 0 {\n\t\t\t\tb.WriteString(`,`)\n\t\t\t}\n\t\t\tprojectTaskJSON, perr := projectTask.MarshalJSON()\n\t\t\tif perr == nil {\n\t\t\t\tb.WriteString(string(projectTaskJSON))\n\t\t\t}\n\t\t}\n\t}\n\n\tb.WriteString(`],`)\n\n\tb.WriteString(`\"projectBody\":\"`)\n\tb.WriteString(p.Body)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectDue\":\"`)\n\tb.WriteString(p.Due)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (ps ProjectsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"projects\":[`)\n\tfor i, project := range ps.Projects {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tprojectJSON, perr := project.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(projectJSON))\n\t\t}\n\t}\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (c Collection) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"collectionId\":\"`)\n\tb.WriteString(c.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"collectionRealmId\":\"`)\n\tb.WriteString(c.RealmId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"collectionName\":\"`)\n\tb.WriteString(c.Name)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"collectionTasks\":[`)\n\tfor i, collectionTask := range c.Tasks {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcollectionTaskJSON, perr := collectionTask.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(collectionTaskJSON))\n\t\t}\n\t}\n\tb.WriteString(`],`)\n\n\tb.WriteString(`\"collectionProjects\":[`)\n\tfor i, collectionProject := range c.Projects {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcollectionProjectJSON, perr := collectionProject.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(collectionProjectJSON))\n\t\t}\n\t}\n\tb.WriteString(`],`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (co CollectionsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"collections\":[`)\n\tfor i, collection := range co.Collections {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcollectionJSON, perr := collection.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(collectionJSON))\n\t\t}\n\t}\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (r Realm) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"realmId\":\"`)\n\tb.WriteString(r.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"realmName\":\"`)\n\tb.WriteString(r.Name)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (rs RealmsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"realms\":[`)\n\t\n\tfor i, realm := range rs.Realms {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\trealmJSON, rerr := realm.MarshalJSON()\n\t\tif rerr == nil {\n\t\t\tb.WriteString(string(realmJSON))\n\t\t}\n\t}\n\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (op ObjectPath) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"objectType\":\"`)\n\tb.WriteString(op.ObjectType)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"id\":\"`)\n\tb.WriteString(op.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"realmId\":\"`)\n\tb.WriteString(op.RealmId)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (oj ObjectJourney) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"objectJourney\":[`)\n\t\n\tfor i, objectPath := range oj.ObjectPaths {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tobjectPathJSON, oerr := objectPath.MarshalJSON()\n\t\tif oerr == nil {\n\t\t\tb.WriteString(string(objectPathJSON))\n\t\t}\n\t}\n\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n"
+ },
+ {
+ "name": "projects.gno",
+ "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n\ntype Project struct {\n\tId \t\t\tstring `json:\"projectId\"`\n\tContextId\tstring `json:\"projectContextId\"`\n\tRealmId \tstring `json:\"projectRealmId\"`\n\tTasks\t\t[]Task `json:\"projectTasks\"`\n\tBody \t\tstring `json:\"projectBody\"`\n\tDue\t\t\tstring `json:\"projectDue\"`\n}\n\ntype ZProjectManager struct {\n\tProjects *avl.Tree // projectId -> Project\n\tProjectTasks *avl.Tree // projectId -> []Task\n}\n\n\nfunc NewZProjectManager() *ZProjectManager {\n\treturn &ZProjectManager{\n\t\tProjects: avl.NewTree(),\n\t\tProjectTasks: avl.NewTree(),\n\t}\n}\n\n// actions\n\nfunc (zpm *ZProjectManager) AddProject(p Project) (err error) {\n\t// implementation\n\n\tif zpm.Projects.Size() != 0 {\n\t\t_, exist := zpm.Projects.Get(p.Id)\n\t\tif exist {\n\t\t\treturn ErrProjectIdAlreadyExists\n\t\t}\n\t}\n\tzpm.Projects.Set(p.Id, p)\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) RemoveProject(p Project) (err error) {\n\t// implementation, remove from ProjectTasks too\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\t // project is removable only in Asses (RealmId 1) or via a Collection (RealmId 4)\n\tif existingProject.RealmId != \"1\" && existingProject.RealmId != \"4\" {\n\t\treturn ErrProjectNotRemovable\n\t}\n\n\t_, removed := zpm.Projects.Remove(existingProject.Id)\n\tif !removed {\n\t\treturn ErrProjectNotRemoved\n\t}\n\n\t// manage project tasks, if any\n\n\tif zpm.ProjectTasks.Size() != 0 {\n\t\t_, exist := zpm.ProjectTasks.Get(existingProject.Id)\n\t\tif !exist {\n\t\t\t// if there's no record in ProjectTasks, we don't have to remove anything\n\t\t\treturn nil\n\t\t} else {\n\t\t\t_, removed := zpm.ProjectTasks.Remove(existingProject.Id)\n\t\t\tif !removed {\n\t\t\t\treturn ErrProjectTasksNotRemoved\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) EditProject(p Project) (err error) {\n\t// implementation, get project by Id and replace the object\n\t// this is for the project body and realm, project tasks are managed in the Tasks object\n\texistingProject := Project{}\n\tif zpm.Projects.Size() != 0 {\n\t\t_, exist := zpm.Projects.Get(p.Id)\n\t\tif !exist {\n\t\t\treturn ErrProjectIdNotFound\n\t\t}\n\t}\n\t\n\t// project Body is editable only when project is in Assess, RealmId = \"1\"\n\tif p.RealmId != \"1\" {\n\t\tif p.Body != existingProject.Body {\n\t\t\treturn ErrProjectNotInAssessRealm\n\t\t}\n\t}\n\n\tzpm.Projects.Set(p.Id, p)\n\treturn nil\n}\n\n// helper function, we can achieve the same with EditProject() above\n/*func (zpm *ZProjectManager) MoveProjectToRealm(projectId string, realmId string) (err error) {\n\t// implementation\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\texistingProject.RealmId = realmId\n\tzpm.Projects.Set(projectId, existingProject)\n\treturn nil\n}*/\n\nfunc (zpm *ZProjectManager) MoveProjectToRealm(projectId string, realmId string) error {\n\t// Get the existing project from the Projects map\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\t// Set the project's RealmId to the new RealmId\n\texistingProject.RealmId = realmId\n\n\t// Get the existing project tasks from the ProjectTasks map\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n\tif !texist {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\ttasks, ok := existingProjectTasksInterface.([]Task)\n\tif !ok {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\n\t// Iterate through the project's tasks and set their RealmId to the new RealmId\n\tfor i := range tasks {\n\t\ttasks[i].RealmId = realmId\n\t}\n\n\t// Set the updated tasks back into the ProjectTasks map\n\tzpm.ProjectTasks.Set(projectId, tasks)\n\n\t// Set the updated project back into the Projects map\n\tzpm.Projects.Set(projectId, existingProject)\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) MarkProjectTaskAsDone(projectId string, projectTaskId string) error {\n // Get the existing project from the Projects map\n existingProjectInterface, exist := zpm.Projects.Get(projectId)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n // Get the existing project tasks from the ProjectTasks map\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n\n // Iterate through the project's tasks to find the task to be updated\n var taskFound bool\n for i, task := range tasks {\n if task.Id == projectTaskId {\n tasks[i].RealmId = \"4\" // Change the RealmId to \"4\"\n taskFound = true\n break\n }\n }\n\n if !taskFound {\n return ErrTaskByIdNotFound\n }\n\n // Set the updated tasks back into the ProjectTasks map\n zpm.ProjectTasks.Set(existingProject.Id, tasks)\n\n return nil\n}\n\n\nfunc (zpm *ZProjectManager) GetProjectTasks(p Project) (tasks []Task, err error) {\n\t// implementation, query ProjectTasks and return the []Tasks object\n\tvar existingProjectTasks []Task\n\n\tif zpm.ProjectTasks.Size() != 0 {\n\t\tprojectTasksInterface, exist := zpm.ProjectTasks.Get(p.Id)\n\t\tif !exist {\n\t\t\treturn nil, ErrProjectTasksNotFound\n\t\t}\n\t\texistingProjectTasks = projectTasksInterface.([]Task)\n\t\treturn existingProjectTasks, nil\n\t}\n\treturn nil, nil\n}\n\nfunc (zpm *ZProjectManager) SetProjectDueDate(projectId string, dueDate string) (err error) {\n\tprojectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\tproject := projectInterface.(Project)\n\n\t// check to see if project is in RealmId = 2 (Decide)\n\tif project.RealmId == \"2\" {\n\t\tproject.Due = dueDate\n\t\tzpm.Projects.Set(project.Id, project)\n\t} else {\n\t\treturn ErrProjectNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) SetProjectTaskDueDate(projectId string, projectTaskId string, dueDate string) (err error){\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n\tif existingProject.RealmId != \"2\" {\n\t\treturn ErrProjectNotEditable\n\t}\n\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n existingProject.Tasks = tasks\n\n var index int = -1\n for i, task := range existingProject.Tasks {\n if task.Id == projectTaskId {\n index = i\n break\n }\n }\n\n if index != -1 {\n existingProject.Tasks[index].Due = dueDate\n } else {\n return ErrTaskByIdNotFound\n }\n\n zpm.ProjectTasks.Set(projectId, existingProject.Tasks)\n return nil\n}\n\n// getters\n\nfunc (zpm *ZProjectManager) GetProjectById(projectId string) (Project, error) {\n\tif zpm.Projects.Size() != 0 {\n\t\tpInterface, exist := zpm.Projects.Get(projectId)\n\t\tif exist {\n\t\t\treturn pInterface.(Project), nil\n\t\t}\n\t}\n\treturn Project{}, ErrProjectIdNotFound\n}\n\nfunc (zpm *ZProjectManager) GetAllProjects() (projects string) {\n\t// implementation\n\tvar allProjects []Project\n\t\n\t// Iterate over the Projects AVL tree to collect all Project objects.\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif project, ok := value.(Project); ok {\n\t\t\t// get project tasks, if any\n\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\tif projectTasks != nil {\n\t\t\t\tproject.Tasks = projectTasks\n\t\t\t}\n\t\t\tallProjects = append(allProjects, project)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: allProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n}\n\nfunc (zpm *ZProjectManager) GetProjectsByRealm(realmId string) (projects string) {\n\t// implementation\n\tvar realmProjects []Project\n\t\n\t// Iterate over the Projects AVL tree to collect all Project objects.\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif project, ok := value.(Project); ok {\n\t\t\tif project.RealmId == realmId {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\trealmProjects = append(realmProjects, project)\n\t\t\t}\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: realmProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n}\n\nfunc (zpm *ZProjectManager) GetProjectsByContextAndRealm(contextId string, realmId string) (projects string) {\n\t// implementation\n\tvar contextProjects []Project\n\t\n\t// Iterate over the Projects AVL tree to collect all Project objects.\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif project, ok := value.(Project); ok {\n\t\t\tif project.ContextId == contextId && project.RealmId == realmId {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tcontextProjects = append(contextProjects, project)\n\t\t\t}\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: contextProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n}\n\nfunc (zpm *ZProjectManager) GetProjectsByDate(projectDate string, filterType string) (projects string) {\n\t// implementation\n\tparsedDate, err:= time.Parse(\"2006-01-02\", projectDate)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tvar filteredProjects []Project\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tproject, ok := value.(Project)\n\t\tif !ok {\n\t\t\treturn false // Skip this iteration and continue.\n\t\t}\n\n\t\tstoredDate, serr := time.Parse(\"2006-01-02\", project.Due)\n\t\tif serr != nil {\n\t\t\t// Skip projects with invalid dates.\n\t\t\treturn false\n\t\t}\n\n\t\tswitch filterType {\n\t\tcase \"specific\":\n\t\t\tif storedDate.Format(\"2006-01-02\") == parsedDate.Format(\"2006-01-02\") {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tfilteredProjects = append(filteredProjects, project)\n\t\t\t}\n\t\tcase \"before\":\n\t\t\tif storedDate.Before(parsedDate) {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tfilteredProjects = append(filteredProjects, project)\n\t\t\t}\n\t\tcase \"after\":\n\t\t\tif storedDate.After(parsedDate) {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tfilteredProjects = append(filteredProjects, project)\n\t\t\t}\n\t\t}\n\n\t\treturn false // Continue iteration.\n\t})\n\n\tif len(filteredProjects) == 0 {\n\t\treturn \"\"\n\t}\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: filteredProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n\n}\n"
+ },
+ {
+ "name": "projects_test.gno",
+ "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n/*\nfunc Test_AddProject(t *testing.T) {\n \n project := Project{Id: \"1\", RealmId: \"1\", Body: \"First project\", ContextId: \"1\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n // Test adding a duplicate project.\n cerr := project.AddProject()\n if cerr != ErrProjectIdAlreadyExists {\n t.Errorf(\"Expected ErrProjectIdAlreadyExists, got %v\", cerr)\n }\n}\n\n\nfunc Test_RemoveProject(t *testing.T) {\n \n project := Project{Id: \"20\", Body: \"Removable project\", RealmId: \"1\", ContextId: \"2\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n retrievedProject, rerr := GetProjectById(project.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added project\")\n }\n\n // Test removing a project\n terr := retrievedProject.RemoveProject()\n if terr != ErrProjectNotRemoved {\n t.Errorf(\"Expected ErrProjectNotRemoved, got %v\", terr)\n }\n}\n\n\nfunc Test_EditProject(t *testing.T) {\n \n project := Project{Id: \"2\", Body: \"Second project content\", RealmId: \"1\", ContextId: \"2\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n // Test editing the project\n editedProject := Project{Id: project.Id, Body: \"Edited project content\", RealmId: project.RealmId, ContextId: \"2\",}\n cerr := editedProject.EditProject()\n if cerr != nil {\n t.Errorf(\"Failed to edit the project\")\n }\n\n retrievedProject, _ := GetProjectById(editedProject.Id)\n if retrievedProject.Body != \"Edited project content\" {\n t.Errorf(\"Project was not edited\")\n }\n}\n\n\nfunc Test_MoveProjectToRealm(t *testing.T) {\n \n project := Project{Id: \"3\", Body: \"Project id 3 content\", RealmId: \"1\", ContextId: \"1\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n // Test moving the project to another realm\n \n cerr := project.MoveProjectToRealm(\"2\")\n if cerr != nil {\n t.Errorf(\"Failed to move project to another realm\")\n }\n\n retrievedProject, _ := GetProjectById(project.Id)\n if retrievedProject.RealmId != \"2\" {\n t.Errorf(\"Project was moved to the wrong realm\")\n }\n}\n\nfunc Test_SetProjectDueDate(t *testing.T) {\n\tprojectRealmIdOne, _ := GetProjectById(\"1\")\n projectRealmIdTwo, _ := GetProjectById(\"10\") \n\t// Define test cases\n\ttests := []struct {\n\t\tname string\n\t\tproject Project\n\t\tdueDate string\n\t\twantErr error\n\t}{\n\t\t{\n\t\t\tname: \"Project does not exist\",\n\t\t\tproject: Project{Id: \"nonexistent\", RealmId: \"2\"},\n\t\t\twantErr: ErrProjectIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Project not editable due to wrong realm\",\n\t\t\tproject: projectRealmIdOne,\n\t\t\twantErr: ErrProjectNotEditable,\n\t\t},\n\t\t{\n\t\t\tname: \"Successfully set alert\",\n\t\t\tproject: projectRealmIdTwo,\n\t\t\tdueDate: \"2024-01-01\",\n\t\t\twantErr: nil,\n\t\t},\n\t}\n\n\t// Execute test cases\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.project.SetProjectDueDate(tc.dueDate)\n\n\t\t\t// Validate\n\t\t\tif err != tc.wantErr {\n\t\t\t\tt.Errorf(\"Expected error %v, got %v\", tc.wantErr, err)\n\t\t\t}\n\n\t\t\t// Additional check for the success case to ensure the due date was actually set\n\t\t\tif err == nil {\n\t\t\t\t// Fetch the task again to check if the due date was set correctly\n\t\t\t\tupdatedProject, exist := Projects.Get(tc.project.Id)\n\t\t\t\tif !exist {\n\t\t\t\t\tt.Fatalf(\"Project %v was not found after setting the due date\", tc.project.Id)\n\t\t\t\t}\n\t\t\t\tif updatedProject.(Project).Due != tc.dueDate {\n\t\t\t\t\tt.Errorf(\"Expected due date to be %v, got %v\", tc.dueDate, updatedProject.(Project).Due)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// getters\n\nfunc Test_GetAllProjects(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n knownProjects := []Project{\n {Id: \"1\", Body: \"First project\", RealmId: \"1\", ContextId: \"1\",},\n {Id: \"10\", Body: \"Project 10\", RealmId: \"2\", ContextId: \"2\", Due: \"2024-01-01\"},\n\t\t{Id: \"2\", Body: \"Edited project content\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable project\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"21\", Body: \"Project 21\", RealmId: \"1\",},\n {Id: \"22\", Body: \"Project 22\", RealmId: \"1\",},\n\t\t{Id: \"3\", Body: \"Project id 3 content\", RealmId: \"2\", ContextId: \"1\",},\n }\n\n // Manually marshal the known projects to create the expected outcome.\n projectsObject := ProjectsObject{Projects: knownProjects}\n expected, err := projectsObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known projects: %v\", err)\n }\n\n // Execute GetAllProjects() to get the actual outcome.\n actual, err := GetAllProjects()\n if err != nil {\n t.Fatalf(\"GetAllProjects() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual project JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetProjectsByDate(t *testing.T) {\n\t\n\ttests := []struct {\n\t\tname string\n\t\tprojectDate string\n\t\tfilterType string\n\t\twant string\n\t\twantErr bool\n\t}{\n\t\t{\"SpecificDate\", \"2024-01-01\", \"specific\", `{\"projects\":[{\"projectId\":\"10\",\"projectContextId\":\"2\",\"projectRealmId\":\"2\",\"projectTasks\":[],\"projectBody\":\"Project 10\",\"projectDue\":\"2024-01-01\"}]}`, false},\n\t\t{\"BeforeDate\", \"2022-04-05\", \"before\", \"\", false},\n\t\t{\"AfterDate\", \"2025-04-05\", \"after\", \"\", false},\n\t\t{\"NoMatch\", \"2002-04-07\", \"specific\", \"\", false},\n\t\t{\"InvalidDateFormat\", \"April 5, 2023\", \"specific\", \"\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := GetProjectsByDate(tt.projectDate, tt.filterType)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"GetProjectsByDate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err == nil && got != tt.want {\n\t\t\t\tt.Errorf(\"GetProjectsByDate() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_GetProjectTasks(t *testing.T){\n \n task := Task{Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",}\n\n project, perr := GetProjectById(\"1\")\n if perr != nil {\n t.Errorf(\"GetProjectById() failed, %v\", perr)\n }\n\n // test attaching to an existing project\n atterr := task.AttachTaskToProject(project)\n if atterr != nil {\n t.Errorf(\"AttachTaskToProject() failed, %v\", atterr)\n }\n\n projectTasks, pterr := project.GetProjectTasks()\n if len(projectTasks) == 0 {\n t.Errorf(\"GetProjectTasks() failed, %v\", pterr)\n }\n\n // test detaching from an existing project\n dtterr := task.DetachTaskFromProject(project)\n if dtterr != nil {\n t.Errorf(\"DetachTaskFromProject() failed, %v\", dtterr)\n }\n\n projectWithNoTasks, pterr := project.GetProjectTasks()\n if len(projectWithNoTasks) != 0 {\n t.Errorf(\"GetProjectTasks() after detach failed, %v\", pterr)\n }\n}\n\nfunc Test_GetProjectById(t *testing.T){\n // test getting a non-existing project\n nonProject, err := GetProjectById(\"0\")\n if err != ErrProjectByIdNotFound {\n t.Fatalf(\"Expected ErrProjectByIdNotFound, got: %v\", err)\n }\n\n // test getting the correct task by id\n correctProject, err := GetProjectById(\"1\")\n if err != nil {\n t.Fatalf(\"Failed to get project by id, error: %v\", err)\n }\n\n if correctProject.Body != \"First project\" {\n t.Fatalf(\"Got the wrong project, with body: %v\", correctProject.Body)\n }\n}\n\nfunc Test_GetProjectsByRealm(t *testing.T) {\n \n // mocking the projects based on previous tests\n // TODO: add isolation?\n projectsInAssessRealm := []Project{\n {Id: \"1\", Body: \"First project\", RealmId: \"1\", ContextId: \"1\",},\n\t\t{Id: \"2\", Body: \"Edited project content\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable project\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"21\", Body: \"Project 21\", RealmId: \"1\",},\n {Id: \"22\", Body: \"Project 22\", RealmId: \"1\",},\n }\n\n // Manually marshal the known projects to create the expected outcome.\n projectsObjectAssess := ProjectsObject{Projects: projectsInAssessRealm}\n expected, err := projectsObjectAssess.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal projects in Assess: %v\", err)\n }\n\n actual, err := GetProjectsByRealm(\"1\")\n if err != nil {\n t.Fatalf(\"GetProjectByRealm('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual projects JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetProjectsByContext(t *testing.T) {\n \n // mocking the projects based on previous tests\n // TODO: add isolation?\n projectsInContextOne := []Project{\n {Id: \"1\", Body: \"First project\", RealmId: \"1\", ContextId: \"1\",},\n\t\t{Id: \"3\", Body: \"Project id 3 content\", RealmId: \"2\", ContextId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n projectsObjectForContexts := ProjectsObject{Projects: projectsInContextOne}\n expected, err := projectsObjectForContexts.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal projects for ContextId 1: %v\", err)\n }\n\n actual, err := GetProjectsByContext(\"1\")\n if err != nil {\n t.Fatalf(\"GetProjectsByContext('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual project JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n\n"
+ },
+ {
+ "name": "realms.gno",
+ "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\n// structs\n\ntype Realm struct {\n\tId \t\t\tstring `json:\"realmId\"`\n\tName \t\tstring `json:\"realmName\"`\n}\n\ntype ZRealmManager struct {\n\tRealms *avl.Tree\n}\n\nfunc NewZRealmManager() *ZRealmManager {\n\tzrm := &ZRealmManager{\n\t\tRealms: avl.NewTree(),\n\t}\n\tzrm.initializeHardcodedRealms()\n\treturn zrm\n}\n\n\nfunc (zrm *ZRealmManager) initializeHardcodedRealms() {\n\thardcodedRealms := []Realm{\n\t\t{Id: \"1\", Name: \"Assess\"},\n\t\t{Id: \"2\", Name: \"Decide\"},\n\t\t{Id: \"3\", Name: \"Do\"},\n\t\t{Id: \"4\", Name: \"Collections\"},\n\t}\n\n\tfor _, realm := range hardcodedRealms {\n\t\tzrm.Realms.Set(realm.Id, realm)\n\t}\n}\n\n\nfunc (zrm *ZRealmManager) AddRealm(r Realm) (err error){\n\t// implementation\n\tif zrm.Realms.Size() != 0 {\n\t\t_, exist := zrm.Realms.Get(r.Id)\n\t\tif exist {\n\t\t\treturn ErrRealmIdAlreadyExists\n\t\t}\n\t}\n\t// check for hardcoded values\n\tif r.Id == \"1\" || r.Id == \"2\" || r.Id == \"3\" || r.Id == \"4\" {\n\t\treturn ErrRealmIdNotAllowed\n\t}\n\tzrm.Realms.Set(r.Id, r)\n\treturn nil\n\t\n}\n\nfunc (zrm *ZRealmManager) RemoveRealm(r Realm) (err error){\n\t// implementation\n\tif zrm.Realms.Size() != 0 {\n\t\t_, exist := zrm.Realms.Get(r.Id)\n\t\tif !exist {\n\t\t\treturn ErrRealmIdNotFound\n\t\t} else {\n\t\t\t// check for hardcoded values, not removable\n\t\t\tif r.Id == \"1\" || r.Id == \"2\" || r.Id == \"3\" || r.Id == \"4\" {\n\t\t\t\treturn ErrRealmIdNotAllowed\n\t\t\t}\n\t\t}\n\t}\n\t\n\t_, removed := zrm.Realms.Remove(r.Id)\n\tif !removed {\n\t\treturn ErrRealmNotRemoved\n\t}\n\treturn nil\n\t\n}\n\n// getters\nfunc (zrm *ZRealmManager) GetRealmById(realmId string) (r Realm, err error) {\n\t// implementation\n\tif zrm.Realms.Size() != 0 {\n\t\trInterface, exist := zrm.Realms.Get(realmId)\n\t\tif exist {\n\t\t\treturn rInterface.(Realm), nil\n\t\t} else {\n\t\t\treturn Realm{}, ErrRealmIdNotFound\n\t\t}\n\t}\n\treturn Realm{}, ErrRealmIdNotFound\n}\n\nfunc (zrm *ZRealmManager) GetRealms() (realms string, err error) {\n\t// implementation\n\tvar allRealms []Realm\n\n\t// Iterate over the Realms AVL tree to collect all Context objects.\n\tzrm.Realms.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif realm, ok := value.(Realm); ok {\n\t\t\tallRealms = append(allRealms, realm)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\n\t// Create a RealmsObject with all collected contexts.\n\trealmsObject := &RealmsObject{\n\t\tRealms: allRealms,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the realms into JSON.\n\tmarshalledRealms, rerr := realmsObject.MarshalJSON()\n\tif rerr != nil {\n\t\treturn \"\", rerr\n\t} \n\treturn string(marshalledRealms), nil\n}\n"
+ },
+ {
+ "name": "tasks.gno",
+ "body": "package zentasktic\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\ntype Task struct {\n\tId \t\t\tstring `json:\"taskId\"`\n\tProjectId \tstring `json:\"taskProjectId\"`\n\tContextId\tstring `json:\"taskContextId\"`\n\tRealmId \tstring `json:\"taskRealmId\"`\n\tBody \t\tstring `json:\"taskBody\"`\n\tDue\t\t\tstring `json:\"taskDue\"`\n\tAlert\t\tstring `json:\"taskAlert\"`\n}\n\ntype ZTaskManager struct {\n\tTasks *avl.Tree\n}\n\nfunc NewZTaskManager() *ZTaskManager {\n\treturn &ZTaskManager{\n\t\tTasks: avl.NewTree(),\n\t}\n}\n\n// actions\n\nfunc (ztm *ZTaskManager) AddTask(t Task) error {\n\tif ztm.Tasks.Size() != 0 {\n\t\t_, exist := ztm.Tasks.Get(t.Id)\n\t\tif exist {\n\t\t\treturn ErrTaskIdAlreadyExists\n\t\t}\n\t}\n\tztm.Tasks.Set(t.Id, t)\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) RemoveTask(t Task) error {\n\texistingTaskInterface, exist := ztm.Tasks.Get(t.Id)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\texistingTask := existingTaskInterface.(Task)\n\n\t // task is removable only in Asses (RealmId 1) or via a Collection (RealmId 4)\n\tif existingTask.RealmId != \"1\" && existingTask.RealmId != \"4\" {\n\t\treturn ErrTaskNotRemovable\n\t}\n\n\t_, removed := ztm.Tasks.Remove(existingTask.Id)\n\tif !removed {\n\t\treturn ErrTaskNotRemoved\n\t}\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) EditTask(t Task) error {\n\texistingTaskInterface, exist := ztm.Tasks.Get(t.Id)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\texistingTask := existingTaskInterface.(Task)\n\n\t// task Body is editable only when task is in Assess, RealmId = \"1\"\n\tif t.RealmId != \"1\" {\n\t\tif t.Body != existingTask.Body {\n\t\t\treturn ErrTaskNotInAssessRealm\n\t\t}\n\t}\n\n\tztm.Tasks.Set(t.Id, t)\n\treturn nil\n}\n\n// Helper function to move a task to a different realm\nfunc (ztm *ZTaskManager) MoveTaskToRealm(taskId, realmId string) error {\n\texistingTaskInterface, exist := ztm.Tasks.Get(taskId)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\texistingTask := existingTaskInterface.(Task)\n\texistingTask.RealmId = realmId\n\tztm.Tasks.Set(taskId, existingTask)\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) SetTaskDueDate(taskId, dueDate string) error {\n\ttaskInterface, exist := ztm.Tasks.Get(taskId)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\ttask := taskInterface.(Task)\n\n\tif task.RealmId == \"2\" {\n\t\ttask.Due = dueDate\n\t\tztm.Tasks.Set(task.Id, task)\n\t} else {\n\t\treturn ErrTaskNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) SetTaskAlert(taskId, alertDate string) error {\n\ttaskInterface, exist := ztm.Tasks.Get(taskId)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\ttask := taskInterface.(Task)\n\n\tif task.RealmId == \"2\" {\n\t\ttask.Alert = alertDate\n\t\tztm.Tasks.Set(task.Id, task)\n\t} else {\n\t\treturn ErrTaskNotEditable\n\t}\n\n\treturn nil\n}\n\n// tasks & projects association\n\nfunc (zpm *ZProjectManager) AttachTaskToProject(ztm *ZTaskManager, t Task, p Project) error {\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(p.Id)\n\tif !texist {\n\t\texistingProject.Tasks = []Task{}\n\t} else {\n\t\ttasks, ok := existingProjectTasksInterface.([]Task)\n\t\tif !ok {\n\t\t\treturn ErrProjectTasksNotFound\n\t\t}\n\t\texistingProject.Tasks = tasks\n\t}\n\n\tt.ProjectId = p.Id\n\t// @todo we need to remove it from Tasks if it was previously added there, then detached\n\texistingTask, err := ztm.GetTaskById(t.Id)\n\tif err == nil {\n\t\tztm.RemoveTask(existingTask)\n\t}\n\tupdatedTasks := append(existingProject.Tasks, t)\n\tzpm.ProjectTasks.Set(p.Id, updatedTasks)\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) EditProjectTask(projectTaskId string, projectTaskBody string, projectId string) error {\n existingProjectInterface, exist := zpm.Projects.Get(projectId)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n\tif existingProject.RealmId != \"1\" {\n\t\treturn ErrProjectNotEditable\n\t}\n\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n existingProject.Tasks = tasks\n\n var index int = -1\n for i, task := range existingProject.Tasks {\n if task.Id == projectTaskId {\n index = i\n break\n }\n }\n\n if index != -1 {\n existingProject.Tasks[index].Body = projectTaskBody\n } else {\n return ErrTaskByIdNotFound\n }\n\n zpm.ProjectTasks.Set(projectId, existingProject.Tasks)\n return nil\n}\n\nfunc (zpm *ZProjectManager) DetachTaskFromProject(ztm *ZTaskManager, projectTaskId string, detachedTaskId string, p Project) error {\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(p.Id)\n\tif !texist {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\ttasks, ok := existingProjectTasksInterface.([]Task)\n\tif !ok {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\texistingProject.Tasks = tasks\n\n\tvar foundTask Task\n\tvar index int = -1\n\tfor i, task := range existingProject.Tasks {\n\t\tif task.Id == projectTaskId {\n\t\t\tindex = i\n\t\t\tfoundTask = task\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif index != -1 {\n\t\texistingProject.Tasks = append(existingProject.Tasks[:index], existingProject.Tasks[index+1:]...)\n\t} else {\n\t\treturn ErrTaskByIdNotFound\n\t}\n\n\tfoundTask.ProjectId = \"\"\n\tfoundTask.Id = detachedTaskId\n\t// Tasks and ProjectTasks have different storage, if a task is detached from a Project\n\t// we add it to the Tasks storage\n\tif err := ztm.AddTask(foundTask); err != nil {\n\t\treturn err\n\t}\n\n\tzpm.ProjectTasks.Set(p.Id, existingProject.Tasks)\n\treturn nil\n}\n\n\nfunc (zpm *ZProjectManager) RemoveTaskFromProject(projectTaskId string, projectId string) error {\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n\tif !texist {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\ttasks, ok := existingProjectTasksInterface.([]Task)\n\tif !ok {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\texistingProject.Tasks = tasks\n\n\tvar index int = -1\n\tfor i, task := range existingProject.Tasks {\n\t\tif task.Id == projectTaskId {\n\t\t\tindex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif index != -1 {\n\t\texistingProject.Tasks = append(existingProject.Tasks[:index], existingProject.Tasks[index+1:]...)\n\t} else {\n\t\treturn ErrTaskByIdNotFound\n\t}\n\n\tzpm.ProjectTasks.Set(projectId, existingProject.Tasks)\n\treturn nil\n}\n\n// getters\n\nfunc (ztm *ZTaskManager) GetTaskById(taskId string) (Task, error) {\n\tif ztm.Tasks.Size() != 0 {\n\t\ttInterface, exist := ztm.Tasks.Get(taskId)\n\t\tif exist {\n\t\t\treturn tInterface.(Task), nil\n\t\t}\n\t}\n\treturn Task{}, ErrTaskIdNotFound\n}\n\nfunc (ztm *ZTaskManager) GetAllTasks() (task string) {\n\tvar allTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif task, ok := value.(Task); ok {\n\t\t\tallTasks = append(allTasks, task)\n\t\t}\n\t\treturn false\n\t})\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: allTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\t\n}\n\nfunc (ztm *ZTaskManager) GetTasksByRealm(realmId string) (tasks string) {\n\tvar realmTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif task, ok := value.(Task); ok {\n\t\t\tif task.RealmId == realmId {\n\t\t\t\trealmTasks = append(realmTasks, task)\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: realmTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\n}\n\nfunc (ztm *ZTaskManager) GetTasksByContextAndRealm(contextId string, realmId string) (tasks string) {\n\tvar contextTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif task, ok := value.(Task); ok {\n\t\t\tif task.ContextId == contextId && task.ContextId == realmId {\n\t\t\t\tcontextTasks = append(contextTasks, task)\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: contextTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\n}\n\nfunc (ztm *ZTaskManager) GetTasksByDate(taskDate string, filterType string) (tasks string) {\n\tparsedDate, err := time.Parse(\"2006-01-02\", taskDate)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tvar filteredTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\ttask, ok := value.(Task)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\t\tstoredDate, serr := time.Parse(\"2006-01-02\", task.Due)\n\t\tif serr != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tswitch filterType {\n\t\tcase \"specific\":\n\t\t\tif storedDate.Format(\"2006-01-02\") == parsedDate.Format(\"2006-01-02\") {\n\t\t\t\tfilteredTasks = append(filteredTasks, task)\n\t\t\t}\n\t\tcase \"before\":\n\t\t\tif storedDate.Before(parsedDate) {\n\t\t\t\tfilteredTasks = append(filteredTasks, task)\n\t\t\t}\n\t\tcase \"after\":\n\t\t\tif storedDate.After(parsedDate) {\n\t\t\t\tfilteredTasks = append(filteredTasks, task)\n\t\t\t}\n\t\t}\n\n\t\treturn false\n\t})\n\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: filteredTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\n}\n"
+ },
+ {
+ "name": "tasks_test.gno",
+ "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n\n// Shared instance of ZTaskManager\nvar ztm *ZTaskManager\n\nfunc init() {\n ztm = NewZTaskManager()\n}\n\nfunc Test_AddTask(t *testing.T) {\n task := Task{Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",}\n\n // Test adding a task successfully.\n err := ztm.AddTask(task)\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n // Test adding a duplicate task.\n cerr := ztm.AddTask(task)\n if cerr != ErrTaskIdAlreadyExists {\n t.Errorf(\"Expected ErrTaskIdAlreadyExists, got %v\", cerr)\n }\n}\n\nfunc Test_RemoveTask(t *testing.T) {\n \n task := Task{Id: \"20\", Body: \"Removable task\", RealmId: \"1\"}\n\n // Test adding a task successfully.\n err := ztm.AddTask(task)\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n retrievedTask, rerr := ztm.GetTaskById(task.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added task\")\n }\n\n // Test removing a task\n terr := ztm.RemoveTask(retrievedTask)\n if terr != nil {\n t.Errorf(\"Expected nil, got %v\", terr)\n }\n}\n\nfunc Test_EditTask(t *testing.T) {\n \n task := Task{Id: \"2\", Body: \"First content\", RealmId: \"1\", ContextId: \"2\"}\n\n // Test adding a task successfully.\n err := ztm.AddTask(task)\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n // Test editing the task\n editedTask := Task{Id: task.Id, Body: \"Edited content\", RealmId: task.RealmId, ContextId: \"2\"}\n cerr := ztm.EditTask(editedTask)\n if cerr != nil {\n t.Errorf(\"Failed to edit the task\")\n }\n\n retrievedTask, _ := ztm.GetTaskById(editedTask.Id)\n if retrievedTask.Body != \"Edited content\" {\n t.Errorf(\"Task was not edited\")\n }\n}\n/*\nfunc Test_MoveTaskToRealm(t *testing.T) {\n \n task := Task{Id: \"3\", Body: \"First content\", RealmId: \"1\", ContextId: \"1\"}\n\n // Test adding a task successfully.\n err := task.AddTask()\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n // Test moving the task to another realm\n \n cerr := task.MoveTaskToRealm(\"2\")\n if cerr != nil {\n t.Errorf(\"Failed to move task to another realm\")\n }\n\n retrievedTask, _ := GetTaskById(task.Id)\n if retrievedTask.RealmId != \"2\" {\n t.Errorf(\"Task was moved to the wrong realm\")\n }\n}\n\nfunc Test_AttachTaskToProject(t *testing.T) {\n \n // Example Projects and Tasks\n prj := Project{Id: \"1\", Body: \"Project 1\", RealmId: \"1\",}\n tsk := Task{Id: \"4\", Body: \"Task 4\", RealmId: \"1\",}\n\n Projects.Set(prj.Id, prj) // Mock existing project\n\n tests := []struct {\n name string\n project Project\n task Task\n wantErr bool\n errMsg error\n }{\n {\n name: \"Attach to existing project\",\n project: prj,\n task: tsk,\n wantErr: false,\n },\n {\n name: \"Attach to non-existing project\",\n project: Project{Id: \"200\", Body: \"Project 200\", RealmId: \"1\",},\n task: tsk,\n wantErr: true,\n errMsg: ErrProjectIdNotFound,\n },\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n err := tt.task.AttachTaskToProject(tt.project)\n if (err != nil) != tt.wantErr {\n t.Errorf(\"AttachTaskToProject() error = %v, wantErr %v\", err, tt.wantErr)\n }\n if tt.wantErr && err != tt.errMsg {\n t.Errorf(\"AttachTaskToProject() error = %v, expected %v\", err, tt.errMsg)\n }\n\n // For successful attach, verify the task is added to the project's tasks.\n if !tt.wantErr {\n tasks, exist := ProjectTasks.Get(tt.project.Id)\n if !exist || len(tasks.([]Task)) == 0 {\n t.Errorf(\"Task was not attached to the project\")\n } else {\n found := false\n for _, task := range tasks.([]Task) {\n if task.Id == tt.task.Id {\n found = true\n break\n }\n }\n if !found {\n t.Errorf(\"Task was not attached to the project\")\n }\n }\n }\n })\n }\n}\n\nfunc TestDetachTaskFromProject(t *testing.T) {\n\t\n\t// Setup:\n\tproject := Project{Id: \"p1\", Body: \"Test Project\"}\n\ttask1 := Task{Id: \"5\", Body: \"Task One\"}\n\ttask2 := Task{Id: \"6\", Body: \"Task Two\"}\n\n\tProjects.Set(project.Id, project)\n\tProjectTasks.Set(project.Id, []Task{task1, task2})\n\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\tproject Project\n\t\twantErr bool\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"Detach existing task from project\",\n\t\t\ttask: task1,\n\t\t\tproject: project,\n\t\t\twantErr: false,\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to detach task from non-existing project\",\n\t\t\ttask: task1,\n\t\t\tproject: Project{Id: \"nonexistent\"},\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrProjectIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to detach non-existing task from project\",\n\t\t\ttask: Task{Id: \"nonexistent\"},\n\t\t\tproject: project,\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrTaskByIdNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.task.DetachTaskFromProject(tt.project)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil || err != tt.expectedErr {\n\t\t\t\t\tt.Errorf(\"%s: expected error %v, got %v\", tt.name, tt.expectedErr, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"%s: unexpected error: %v\", tt.name, err)\n\t\t\t\t}\n\n\t\t\t\t// For successful detachment, verify the task is no longer part of the project's tasks\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\ttasks, _ := ProjectTasks.Get(tt.project.Id)\n\t\t\t\t\tfor _, task := range tasks.([]Task) {\n\t\t\t\t\t\tif task.Id == tt.task.Id {\n\t\t\t\t\t\t\tt.Errorf(\"%s: task was not detached from the project\", tt.name)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SetTaskDueDate(t *testing.T) {\n\ttaskRealmIdOne, _ := GetTaskById(\"1\")\n taskRealmIdTwo, _ := GetTaskById(\"10\") \n\t// Define test cases\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\tdueDate string\n\t\twantErr error\n\t}{\n\t\t{\n\t\t\tname: \"Task does not exist\",\n\t\t\ttask: Task{Id: \"nonexistent\", RealmId: \"2\"},\n\t\t\twantErr: ErrTaskIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Task not editable due to wrong realm\",\n\t\t\ttask: taskRealmIdOne,\n\t\t\twantErr: ErrTaskNotEditable,\n\t\t},\n\t\t{\n\t\t\tname: \"Successfully set due date\",\n\t\t\ttask: taskRealmIdTwo,\n\t\t\tdueDate: \"2023-01-01\",\n\t\t\twantErr: nil,\n\t\t},\n\t}\n\n\t// Execute test cases\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.task.SetTaskDueDate(tc.dueDate)\n\n\t\t\t// Validate\n\t\t\tif err != tc.wantErr {\n\t\t\t\tt.Errorf(\"Expected error %v, got %v\", tc.wantErr, err)\n\t\t\t}\n\n\t\t\t// Additional check for the success case to ensure the due date was actually set\n\t\t\tif err == nil {\n\t\t\t\t// Fetch the task again to check if the due date was set correctly\n\t\t\t\tupdatedTask, exist := Tasks.Get(tc.task.Id)\n\t\t\t\tif !exist {\n\t\t\t\t\tt.Fatalf(\"Task %v was not found after setting the due date\", tc.task.Id)\n\t\t\t\t}\n\t\t\t\tif updatedTask.(Task).Due != tc.dueDate {\n\t\t\t\t\tt.Errorf(\"Expected due date to be %v, got %v\", tc.dueDate, updatedTask.(Task).Due)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SetTaskAlert(t *testing.T) {\n\ttaskRealmIdOne, _ := GetTaskById(\"1\")\n taskRealmIdTwo, _ := GetTaskById(\"10\") \n\t// Define test cases\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\talertDate string\n\t\twantErr error\n\t}{\n\t\t{\n\t\t\tname: \"Task does not exist\",\n\t\t\ttask: Task{Id: \"nonexistent\", RealmId: \"2\"},\n\t\t\twantErr: ErrTaskIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Task not editable due to wrong realm\",\n\t\t\ttask: taskRealmIdOne,\n\t\t\twantErr: ErrTaskNotEditable,\n\t\t},\n\t\t{\n\t\t\tname: \"Successfully set alert\",\n\t\t\ttask: taskRealmIdTwo,\n\t\t\talertDate: \"2024-01-01\",\n\t\t\twantErr: nil,\n\t\t},\n\t}\n\n\t// Execute test cases\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.task.SetTaskAlert(tc.alertDate)\n\n\t\t\t// Validate\n\t\t\tif err != tc.wantErr {\n\t\t\t\tt.Errorf(\"Expected error %v, got %v\", tc.wantErr, err)\n\t\t\t}\n\n\t\t\t// Additional check for the success case to ensure the due date was actually set\n\t\t\tif err == nil {\n\t\t\t\t// Fetch the task again to check if the due date was set correctly\n\t\t\t\tupdatedTask, exist := Tasks.Get(tc.task.Id)\n\t\t\t\tif !exist {\n\t\t\t\t\tt.Fatalf(\"Task %v was not found after setting the due date\", tc.task.Id)\n\t\t\t\t}\n\t\t\t\tif updatedTask.(Task).Alert != tc.alertDate {\n\t\t\t\t\tt.Errorf(\"Expected due date to be %v, got %v\", tc.alertDate, updatedTask.(Task).Due)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// getters\n\nfunc Test_GetAllTasks(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n knownTasks := []Task{\n {Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",},\n {Id: \"10\", Body: \"First content\", RealmId: \"2\", ContextId: \"2\", Due: \"2023-01-01\", Alert: \"2024-01-01\"},\n {Id: \"2\", Body: \"Edited content\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable task\", RealmId: \"1\",},\n {Id: \"3\", Body: \"First content\", RealmId: \"2\", ContextId: \"1\",},\n {Id: \"40\", Body: \"Task 40\", RealmId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n tasksObject := TasksObject{Tasks: knownTasks}\n expected, err := tasksObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known tasks: %v\", err)\n }\n\n // Execute GetAllTasks() to get the actual outcome.\n actual, err := GetAllTasks()\n if err != nil {\n t.Fatalf(\"GetAllTasks() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual task JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetTasksByDate(t *testing.T) {\n\t\n\ttests := []struct {\n\t\tname string\n\t\ttaskDate string\n\t\tfilterType string\n\t\twant string\n\t\twantErr bool\n\t}{\n\t\t{\"SpecificDate\", \"2023-01-01\", \"specific\", `{\"tasks\":[{\"taskId\":\"10\",\"taskProjectId\":\"\",\"taskContextId\":\"2\",\"taskRealmId\":\"2\",\"taskBody\":\"First content\",\"taskDue\":\"2023-01-01\",\"taskAlert\":\"2024-01-01\"}]}`, false},\n\t\t{\"BeforeDate\", \"2022-04-05\", \"before\", \"\", false},\n\t\t{\"AfterDate\", \"2023-04-05\", \"after\", \"\", false},\n\t\t{\"NoMatch\", \"2002-04-07\", \"specific\", \"\", false},\n\t\t{\"InvalidDateFormat\", \"April 5, 2023\", \"specific\", \"\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := GetTasksByDate(tt.taskDate, tt.filterType)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"GetTasksByDate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err == nil && got != tt.want {\n\t\t\t\tt.Errorf(\"GetTasksByDate() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_GetTaskById(t *testing.T){\n // test getting a non-existing task\n nonTask, err := GetTaskById(\"0\")\n if err != ErrTaskByIdNotFound {\n t.Fatalf(\"Expected ErrTaskByIdNotFound, got: %v\", err)\n }\n\n // test getting the correct task by id\n correctTask, err := GetTaskById(\"1\")\n if err != nil {\n t.Fatalf(\"Failed to get task by id, error: %v\", err)\n }\n\n if correctTask.Body != \"First task\" {\n t.Fatalf(\"Got the wrong task, with body: %v\", correctTask.Body)\n }\n}\n\nfunc Test_GetTasksByRealm(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n tasksInAssessRealm := []Task{\n {Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",},\n {Id: \"2\", RealmId: \"1\", Body: \"Edited content\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable task\", RealmId: \"1\",},\n {Id: \"40\", Body: \"Task 40\", RealmId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n tasksObjectAssess := TasksObject{Tasks: tasksInAssessRealm}\n expected, err := tasksObjectAssess.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal tasks in Assess: %v\", err)\n }\n\n actual, err := GetTasksByRealm(\"1\")\n if err != nil {\n t.Fatalf(\"GetTasksByRealm('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual task JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetTasksByContext(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n tasksInContextOne := []Task{\n {Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",},\n {Id: \"3\", RealmId: \"2\", Body: \"First content\", ContextId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n tasksObjectForContexts := TasksObject{Tasks: tasksInContextOne}\n expected, err := tasksObjectForContexts.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal tasks for ContextId 1: %v\", err)\n }\n\n actual, err := GetTasksByContext(\"1\")\n if err != nil {\n t.Fatalf(\"GetTasksByContext('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual task JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n"
+ }
+ ]
+ },
+ "deposit": ""
+ }
+ ],
+ "fee": {
+ "gas_wanted": "35000000",
+ "gas_fee": "1000000ugnot"
+ },
+ "signatures": [],
+ "memo": ""
+}
+
+-- tx3.tx --
+{
+ "msg": [
+ {
+ "@type": "/vm.m_addpkg",
+ "creator": "g1lmgyf29g6zqgpln5pq05zzt7qkz2wga7xgagv4",
+ "package": {
+ "name": "zentasktic_core",
+ "path": "gno.land/r/g17ernafy6ctpcz6uepfsq2js8x2vz0wladh5yc3/zentasktic_core",
+ "files": [
+ {
+ "name": "workable.gno",
+ "body": "package zentasktic_core\n\ntype Workable interface {\n\t// restrict implementation of Workable to this realm\n\tassertWorkable()\n}\n\ntype isWorkable struct {}\n\nfunc (wt *WorkableTask) assertWorkable() {}\n\nfunc (wp *WorkableProject) assertWorkable() {}\n\nvar _ Workable = &WorkableTask{}\nvar _ Workable = &WorkableProject{}\n"
+ },
+ {
+ "name": "wrapper.gno",
+ "body": "package zentasktic_core\n\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/g17ernafy6ctpcz6uepfsq2js8x2vz0wladh5yc3/zentasktic\"\n)\n\n// this is a convenience wrapper on top of the functions declared in the zentasktic package\n// to maintain consistency\n\n// wrapping zentasktic types\n\ntype WorkableTask struct {\n zentasktic.Task\n}\ntype WorkableProject struct {\n zentasktic.Project\n}\n\ntype WorkableRealm struct {\n\tId string\n\tName string\n}\n\ntype WorkableContext struct {\n\tzentasktic.Context\n}\n\ntype WorkableCollection struct {\n\tzentasktic.Collection\n}\n\ntype WorkableObjectPath struct {\n\tzentasktic.ObjectPath\n}\n\n// zentasktic managers\n\nvar ztm *zentasktic.ZTaskManager\nvar zpm *zentasktic.ZProjectManager\nvar zrm *zentasktic.ZRealmManager\nvar zcm *zentasktic.ZContextManager\nvar zcl *zentasktic.ZCollectionManager\nvar zom *zentasktic.ZObjectPathManager\nvar currentTaskID int\nvar currentProjectTaskID int\nvar currentProjectID int\nvar currentContextID int\nvar currentCollectionID int\nvar currentPathID int\n\nfunc init() {\n ztm = zentasktic.NewZTaskManager()\n zpm = zentasktic.NewZProjectManager()\n\tzrm = zentasktic.NewZRealmManager()\n\tzcm = zentasktic.NewZContextManager()\n\tzcl = zentasktic.NewZCollectionManager()\n\tzom = zentasktic.NewZObjectPathManager()\n\tcurrentTaskID = 0\n\tcurrentProjectTaskID = 0\n\tcurrentProjectID = 0\n\tcurrentContextID = 0\n\tcurrentCollectionID = 0\n\tcurrentPathID = 0\n}\n\n// tasks\n\nfunc AddTask(taskBody string) error {\n\ttaskID := incrementTaskID()\n\twt := &WorkableTask{\n\t\tTask: zentasktic.Task{\n\t\t\tId: strconv.Itoa(taskID),\n\t\t\tBody: taskBody,\n\t\t\tRealmId: \t \"1\",\n\t\t},\n\t}\n\treturn ztm.AddTask(wt.Task)\n}\n\n\nfunc EditTask(taskId string, taskBody string) error {\n\ttaskToEdit, err := GetTaskById(taskId)\n\tif err != nil {\n\t\treturn err\t\n\t}\n\ttaskToEdit.Body = taskBody;\n\treturn ztm.EditTask(taskToEdit.Task)\n}\n\nfunc RemoveTask(taskId string) error {\n\ttaskToRemove, err := GetTaskById(taskId)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn ztm.RemoveTask(taskToRemove.Task)\n}\n\nfunc MoveTaskToRealm(taskId string, realmId string) error {\n\treturn ztm.MoveTaskToRealm(taskId, realmId)\n}\n\nfunc SetTaskDueDate(taskId string, dueDate string) error {\n\treturn ztm.SetTaskDueDate(taskId, dueDate)\n}\n\nfunc SetTaskAlert(taskId string, alert string) error {\n\treturn ztm.SetTaskAlert(taskId, alert)\n}\n\nfunc AttachTaskToProject(taskBody string, projectId string) error {\n\tprojectTaskID := incrementProjectTaskID()\n\twt := &WorkableTask{\n\t\tTask: zentasktic.Task{\n\t\t\tId: strconv.Itoa(projectTaskID),\n\t\t\tBody: taskBody,\n\t\t\tRealmId: \t \"1\",\n\t\t},\n\t}\n\t//ztm.AddTask(wt.Task)\n\tprojectToAdd, err := GetProjectById(projectId)\n\tif err != nil {\n\t\treturn err\t\n\t}\n\treturn zpm.AttachTaskToProject(ztm, wt.Task, projectToAdd.Project)\n}\n\nfunc EditProjectTask(projectTaskId string, projectTaskBody string, projectId string) error {\n\treturn zpm.EditProjectTask(projectTaskId, projectTaskBody, projectId)\n}\n\nfunc DetachTaskFromProject(projectTaskId string, projectId string) error {\n\tprojectToDetachFrom, err := GetProjectById(projectId)\n\tif err != nil {\n\t\treturn err\t\n\t}\n\tdetachedTaskId := strconv.Itoa(incrementTaskID())\n\treturn zpm.DetachTaskFromProject(ztm, projectTaskId, detachedTaskId, projectToDetachFrom.Project)\n}\n\nfunc RemoveTaskFromProject(projectTaskId string, projectId string) error {\n\treturn zpm.RemoveTaskFromProject(projectTaskId, projectId)\n}\n\nfunc GetTaskById(taskId string) (WorkableTask, error) {\n\ttask, err := ztm.GetTaskById(taskId)\n\tif err != nil {\n\t\treturn WorkableTask{}, err\n\t}\n\treturn WorkableTask{Task: task}, nil\n}\n\nfunc GetAllTasks() (string){\n\treturn ztm.GetAllTasks()\n}\n\nfunc GetTasksByRealm(realmId string) (string){\n\treturn ztm.GetTasksByRealm(realmId)\n}\n\nfunc GetTasksByContextAndRealm(contextId string, realmId string) (string){\n\treturn ztm.GetTasksByContextAndRealm(contextId, realmId)\n}\n\nfunc GetTasksByDate(dueDate string, filterType string) (string){\n\treturn ztm.GetTasksByDate(dueDate, filterType)\n}\n\nfunc incrementTaskID() int {\n\tcurrentTaskID++\n\treturn currentTaskID\n}\n\nfunc incrementProjectTaskID() int {\n\tcurrentProjectTaskID++\n\treturn currentProjectTaskID\n}\n\n// projects\n\nfunc AddProject(projectBody string) error {\n\tprojectID := incrementProjectID()\n\twp := &WorkableProject{\n\t\tProject: zentasktic.Project{\n\t\t\tId: strconv.Itoa(projectID),\n\t\t\tBody: projectBody,\n\t\t\tRealmId: \t \"1\",\n\t\t},\n\t}\n\treturn zpm.AddProject(wp.Project)\n}\n\nfunc EditProject(projectId string, projectBody string) error {\n\tprojectToEdit, err := GetProjectById(projectId)\n\tif err != nil {\n\t\treturn err\t\n\t}\n\tprojectToEdit.Body = projectBody;\n\treturn zpm.EditProject(projectToEdit.Project)\n}\n\nfunc RemoveProject(projectId string) error {\n\tprojectToRemove, err := GetProjectById(projectId)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn zpm.RemoveProject(projectToRemove.Project)\n}\n\nfunc MoveProjectToRealm(projectId string, realmId string) error {\n\treturn zpm.MoveProjectToRealm(projectId, realmId)\n}\n\nfunc MarkProjectTaskAsDone(projectId string, projectTaskId string) error {\n\treturn zpm.MarkProjectTaskAsDone(projectId, projectTaskId)\n}\n\nfunc GetProjectTasks(wp WorkableProject) ([]WorkableTask, error){\n\ttasks, err := zpm.GetProjectTasks(wp.Project)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Convert []zentasktic.Task to []WorkableTask\n\tvar workableTasks []WorkableTask\n\tfor _, task := range tasks {\n\t\tworkableTasks = append(workableTasks, WorkableTask{Task: task})\n\t}\n\n\treturn workableTasks, nil\n}\n\nfunc SetProjectDueDate(projectId string, dueDate string) error {\n\treturn zpm.SetProjectDueDate(projectId, dueDate)\n}\n\nfunc GetProjectById(projectId string) (WorkableProject, error) {\n\tproject, err := zpm.GetProjectById(projectId)\n\tif err != nil {\n\t\treturn WorkableProject{}, err\n\t}\n\treturn WorkableProject{Project: project}, nil\n}\n\nfunc SetProjectTaskDueDate(projectId string, projectTaskId string, dueDate string) error {\n\treturn zpm.SetProjectTaskDueDate(projectId, projectTaskId, dueDate)\n}\n\nfunc GetAllProjects() (string){\n\treturn zpm.GetAllProjects()\n}\n\nfunc GetProjectsByRealm(realmId string) (string){\n\treturn zpm.GetProjectsByRealm(realmId)\n}\n\nfunc GetProjectsByContextAndRealm(contextId string, realmId string) (string){\n\treturn zpm.GetProjectsByContextAndRealm(contextId, realmId)\n}\n\nfunc GetProjectsByDate(dueDate string, filterType string) (string){\n\treturn zpm.GetProjectsByDate(dueDate, filterType)\n}\n\nfunc incrementProjectID() int {\n\tcurrentProjectID++\n\treturn currentProjectID\n}\n\n// realms\n\nfunc AddRealm(wr WorkableRealm) error {\n\tr := zentasktic.Realm{\n\t\tId: wr.Id,\n\t\tName: wr.Name,\n\t}\n\treturn zrm.AddRealm(r)\n}\n\nfunc RemoveRealm(wr WorkableRealm) error {\n\tr := zentasktic.Realm{\n\t\tId: wr.Id,\n\t\tName: wr.Name,\n\t}\n\treturn zrm.RemoveRealm(r)\n}\n\nfunc GetRealmById(realmId string) (WorkableRealm, error) {\n\tr, err := zrm.GetRealmById(realmId)\n\tif err != nil {\n\t\treturn WorkableRealm{}, err\n\t}\n\treturn WorkableRealm{\n\t\tId: r.Id,\n\t\tName: r.Name,\n\t}, nil\n}\n\nfunc GetAllRealms() (string, error) {\n\treturn zrm.GetRealms()\n}\n\n// contexts\n\nfunc AddContext(contextName string) error {\n\tcontextID := incrementContextID()\n\twc := &WorkableContext{\n\t\tContext: zentasktic.Context{\n\t\t\tId: strconv.Itoa(contextID),\n\t\t\tName: contextName,\n\t\t},\n\t}\n\treturn zcm.AddContext(wc.Context)\n}\n\nfunc EditContext(contextId string, newContext string) error {\n\tcontextToEdit, err := GetContextById(contextId)\n\tif err != nil {\n\t\treturn err\t\n\t}\n\tcontextToEdit.Name = newContext;\n\treturn zcm.EditContext(contextToEdit.Context)\n}\n\nfunc RemoveContext(contextId string) error {\n\tcontextToRemove, err := GetContextById(contextId)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn zcm.RemoveContext(contextToRemove.Context)\n}\n\nfunc AddContextToTask(contextId string, taskId string) error {\n\tcontextToAdd, err := GetContextById(contextId)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttaskToAddContextTo, merr := GetTaskById(taskId)\n\tif merr != nil {\n\t\treturn merr\n\t}\n\treturn zcm.AddContextToTask(ztm, contextToAdd.Context, taskToAddContextTo.Task)\n}\n\nfunc AddContextToProject(contextId string, projectId string) error {\n\tcontextToAdd, err := GetContextById(contextId)\n\tif err != nil {\n\t\treturn err\n\t}\n\tprojectToAddContextTo, merr := GetProjectById(projectId)\n\tif merr != nil {\n\t\treturn merr\n\t}\n\treturn zcm.AddContextToProject(zpm, contextToAdd.Context, projectToAddContextTo.Project)\n}\n\nfunc AddContextToProjectTask(contextId string, projectId string, projectTaskId string) error {\n\tcontextToAdd, err := GetContextById(contextId)\n\tif err != nil {\n\t\treturn err\n\t}\n\tprojectToAddContextTo, merr := GetProjectById(projectId)\n\tif merr != nil {\n\t\treturn merr\n\t}\n\treturn zcm.AddContextToProjectTask(zpm, contextToAdd.Context, projectToAddContextTo.Project, projectTaskId)\n}\n\nfunc GetContextById(contextId string) (WorkableContext, error) {\n\tcontext, err := zcm.GetContextById(contextId)\n\tif err != nil {\n\t\treturn WorkableContext{}, err\n\t}\n\treturn WorkableContext{Context: context}, nil\n}\n\nfunc GetAllContexts() (string) {\n\treturn zcm.GetAllContexts()\n}\n\nfunc incrementContextID() int {\n\tcurrentContextID++\n\treturn currentContextID\n}\n\n// collections\n/*\nfunc AddCollection(wc WorkableCollection) error {\n\tc := zentasktic.Collection{\n\t\tId: wc.Id,\n\t\tRealmId: wc.RealmId,\n\t\tName: wc.Name,\n\t\tTasks: toZentaskticTasks(wc.Tasks),\n\t\tProjects: toZentaskticProjects(wc.Projects),\n\t}\n\treturn zcl.AddCollection(c)\n}\n\nfunc EditCollection(wc WorkableCollection) error {\n\tc := zentasktic.Collection{\n\t\tId: wc.Id,\n\t\tRealmId: wc.RealmId,\n\t\tName: wc.Name,\n\t\tTasks: toZentaskticTasks(wc.Tasks),\n\t\tProjects: toZentaskticProjects(wc.Projects),\n\t}\n\treturn zcl.EditCollection(c)\n}\n\nfunc RemoveCollection(wc WorkableCollection) error {\n\tc := zentasktic.Collection{\n\t\tId: wc.Id,\n\t\tRealmId: wc.RealmId,\n\t\tName: wc.Name,\n\t\tTasks: toZentaskticTasks(wc.Tasks),\n\t\tProjects: toZentaskticProjects(wc.Projects),\n\t}\n\treturn zcl.RemoveCollection(c)\n}\n\nfunc GetCollectionById(collectionId string) (WorkableCollection, error) {\n\tc, err := zcl.GetCollectionById(collectionId)\n\tif err != nil {\n\t\treturn WorkableCollection{}, err\n\t}\n\treturn WorkableCollection{\n\t\tId: c.Id,\n\t\tRealmId: c.RealmId,\n\t\tName: c.Name,\n\t\tTasks: toWorkableTasks(c.Tasks),\n\t\tProjects: toWorkableProjects(c.Projects),\n\t}, nil\n}\n\nfunc GetCollectionTasks(wc WorkableCollection) ([]WorkableTask, error) {\n\tc := zentasktic.Collection{\n\t\tId: wc.Id,\n\t}\n\ttasks, err := zcl.GetCollectionTasks(c)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn toWorkableTasks(tasks), nil\n}\n\nfunc GetCollectionProjects(wc WorkableCollection) ([]WorkableProject, error) {\n\tc := zentasktic.Collection{\n\t\tId: wc.Id,\n\t}\n\tprojects, err := zcl.GetCollectionProjects(c)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn toWorkableProjects(projects), nil\n}\n\nfunc GetAllCollections() (string, error) {\n\treturn zcl.GetAllCollections()\n}\n\n// Helper functions to convert between Workable and zentasktic types\nfunc toZentaskticTasks(tasks []WorkableTask) []zentasktic.Task {\n\tztasks := make([]zentasktic.Task, len(tasks))\n\tfor i, t := range tasks {\n\t\tztasks[i] = t.Task\n\t}\n\treturn ztasks\n}\n\nfunc toWorkableTasks(tasks []zentasktic.Task) []WorkableTask {\n\twtasks := make([]WorkableTask, len(tasks))\n\tfor i, t := range tasks {\n\t\twtasks[i] = WorkableTask{Task: t}\n\t}\n\treturn wtasks\n}\n\nfunc toZentaskticProjects(projects []WorkableProject) []zentasktic.Project {\n\tzprojects := make([]zentasktic.Project, len(projects))\n\tfor i, p := range projects {\n\t\tzprojects[i] = p.Project\n\t}\n\treturn zprojects\n}\n\nfunc toWorkableProjects(projects []zentasktic.Project) []WorkableProject {\n\twprojects := make([]WorkableProject, len(projects))\n\tfor i, p := range projects {\n\t\twprojects[i] = WorkableProject{Project: p}\n\t}\n\treturn wprojects\n}*/\n\n// object Paths\n\nfunc AddPath(wop WorkableObjectPath) error {\n\to := zentasktic.ObjectPath{\n\t\tObjectType: wop.ObjectType,\n\t\tId: wop.Id,\n\t\tRealmId: wop.RealmId,\n\t}\n\treturn zom.AddPath(o)\n}\n\n\nfunc GetObjectJourney(objectType string, objectId string) (string, error) {\n\treturn zom.GetObjectJourney(objectType, objectId)\n}\n"
+ }
+ ]
+ },
+ "deposit": ""
+ }
+ ],
+ "fee": {
+ "gas_wanted": "30000000",
+ "gas_fee": "1000000ugnot"
+ },
+ "signatures": [],
+ "memo": ""
+}
diff --git a/gno.land/pkg/integration/testdata/restart_nonval.txtar b/gno.land/pkg/integration/testdata/restart_nonval.txtar
new file mode 100644
index 00000000000..87b4ad4ecb9
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/restart_nonval.txtar
@@ -0,0 +1,5 @@
+# This txtar tests for starting up a non-validator node; then also restarting it.
+loadpkg gno.land/p/demo/avl
+
+gnoland start -non-validator
+gnoland restart
diff --git a/gno.land/cmd/gnoland/testdata/run.txtar b/gno.land/pkg/integration/testdata/run.txtar
similarity index 100%
rename from gno.land/cmd/gnoland/testdata/run.txtar
rename to gno.land/pkg/integration/testdata/run.txtar
diff --git a/gno.land/pkg/integration/testdata/simulate_gas.txtar b/gno.land/pkg/integration/testdata/simulate_gas.txtar
new file mode 100644
index 00000000000..0dcb9ba424b
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/simulate_gas.txtar
@@ -0,0 +1,28 @@
+# load the package
+loadpkg gno.land/r/simulate $WORK/simulate
+
+# start a new node
+gnoland start
+
+# simulate only
+gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate only test1
+stdout 'GAS USED: 99371'
+
+# simulate skip
+gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate skip test1
+stdout 'GAS USED: 99371' # same as simulate only
+
+
+-- package/package.gno --
+package call_package
+
+func Render() string {
+ return "notok"
+}
+
+-- simulate/simulate.gno --
+package simulate
+
+func Hello() string {
+ return "Hello"
+}
diff --git a/gno.land/cmd/gnoland/testdata/time_simple.txtar b/gno.land/pkg/integration/testdata/time_simple.txtar
similarity index 85%
rename from gno.land/cmd/gnoland/testdata/time_simple.txtar
rename to gno.land/pkg/integration/testdata/time_simple.txtar
index 932a5721695..ace34fa00a5 100644
--- a/gno.land/cmd/gnoland/testdata/time_simple.txtar
+++ b/gno.land/pkg/integration/testdata/time_simple.txtar
@@ -3,7 +3,7 @@
gnoland start
-gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/time_simple -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1
+gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/time_simple -gas-fee 1ugnot -gas-wanted 15000000 -broadcast -chainid=tendermint_test test1
stdout OK!
-- time_simple.gno --
diff --git a/gno.land/pkg/integration/testdata/wugnot.txtar b/gno.land/pkg/integration/testdata/wugnot.txtar
new file mode 100644
index 00000000000..5a63e0148e2
--- /dev/null
+++ b/gno.land/pkg/integration/testdata/wugnot.txtar
@@ -0,0 +1,45 @@
+loadpkg gno.land/r/demo/wugnot
+
+gnoland start
+
+gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 10000000ugnot -gas-wanted 5000000 -args '' -broadcast -chainid=tendermint_test test1
+stdout '# wrapped GNOT \(\$wugnot\)'
+stdout 'Decimals..: 0'
+stdout 'Total supply..: 0'
+stdout 'Known accounts..: 0'
+stdout 'OK!'
+
+gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Deposit -send 12345678ugnot -gas-fee 1000000ugnot -gas-wanted 50000000 -broadcast -chainid=tendermint_test test1
+stdout 'OK!'
+
+gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 5000000 -args '' -broadcast -chainid=tendermint_test test1
+stdout 'Total supply..: 12345678'
+stdout 'Known accounts..: 1'
+stdout 'OK!'
+
+# XXX: use test2 instead (depends on https://github.com/gnolang/gno/issues/1269#issuecomment-1806386069)
+gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Deposit -send 12345678ugnot -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
+stdout 'OK!'
+
+gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 5000000 -args '' -broadcast -chainid=tendermint_test test1
+stdout 'Total supply..: 24691356'
+stdout 'Known accounts..: 1' # should be 2 once we can use test2
+stdout 'OK!'
+
+# XXX: replace hardcoded address with test3
+gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Transfer -gas-fee 1000000ugnot -gas-wanted 100000000 -args 'g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq' -args '10000000' -broadcast -chainid=tendermint_test test1
+stdout 'OK!'
+
+gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 50000000 -args '' -broadcast -chainid=tendermint_test test1
+stdout 'Total supply..: 24691356'
+stdout 'Known accounts..: 2' # should be 3 once we can use test2
+stdout 'OK!'
+
+# XXX: use test3 instead (depends on https://github.com/gnolang/gno/issues/1269#issuecomment-1806386069)
+gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Withdraw -args 10000000 -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
+stdout 'OK!'
+
+gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 100000000 -args '' -broadcast -chainid=tendermint_test test1
+stdout 'Total supply..: 14691356'
+stdout 'Known accounts..: 2' # should be 3 once we can use test2
+stdout 'OK!'
diff --git a/gno.land/pkg/integration/testdata_test.go b/gno.land/pkg/integration/testdata_test.go
new file mode 100644
index 00000000000..ba4d5176df1
--- /dev/null
+++ b/gno.land/pkg/integration/testdata_test.go
@@ -0,0 +1,67 @@
+package integration
+
+import (
+ "os"
+ "strconv"
+ "testing"
+
+ gno_integration "github.com/gnolang/gno/gnovm/pkg/integration"
+ "github.com/rogpeppe/go-internal/testscript"
+ "github.com/stretchr/testify/require"
+)
+
+func TestTestdata(t *testing.T) {
+ t.Parallel()
+
+ flagInMemoryTS, _ := strconv.ParseBool(os.Getenv("INMEMORY_TS"))
+ flagNoSeqTS, _ := strconv.ParseBool(os.Getenv("NO_SEQ_TS"))
+
+ p := gno_integration.NewTestingParams(t, "testdata")
+
+ if coverdir, ok := gno_integration.ResolveCoverageDir(); ok {
+ err := gno_integration.SetupTestscriptsCoverage(&p, coverdir)
+ require.NoError(t, err)
+ }
+
+ // Set up gnoland for testscript
+ err := SetupGnolandTestscript(t, &p)
+ require.NoError(t, err)
+
+ mode := commandKindTesting
+ if flagInMemoryTS {
+ mode = commandKindInMemory
+ }
+
+ origSetup := p.Setup
+ p.Setup = func(env *testscript.Env) error {
+ env.Values[envKeyExecCommand] = mode
+ if origSetup != nil {
+ if err := origSetup(env); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ }
+
+ if flagInMemoryTS && !flagNoSeqTS {
+ testscript.RunT(tSeqShim{t}, p)
+ } else {
+ testscript.Run(t, p)
+ }
+}
+
+type tSeqShim struct{ *testing.T }
+
+// noop Parallel method allow us to run test sequentially
+func (tSeqShim) Parallel() {}
+
+func (t tSeqShim) Run(name string, f func(testscript.T)) {
+ t.T.Run(name, func(t *testing.T) {
+ f(tSeqShim{t})
+ })
+}
+
+func (t tSeqShim) Verbose() bool {
+ return testing.Verbose()
+}
diff --git a/gno.land/pkg/integration/testing.go b/gno.land/pkg/integration/testing.go
deleted file mode 100644
index 0cd3152d888..00000000000
--- a/gno.land/pkg/integration/testing.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package integration
-
-import (
- "errors"
-
- "github.com/rogpeppe/go-internal/testscript"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-// This error is from testscript.Fatalf and is needed to correctly
-// handle the FailNow method.
-// see: https://github.com/rogpeppe/go-internal/blob/32ae33786eccde1672d4ba373c80e1bc282bfbf6/testscript/testscript.go#L799-L812
-var errFailNow = errors.New("fail now!") //nolint:stylecheck
-
-var (
- _ require.TestingT = (*testingTS)(nil)
- _ assert.TestingT = (*testingTS)(nil)
-)
-
-type TestingTS = require.TestingT
-
-type testingTS struct {
- *testscript.TestScript
-}
-
-func TSTestingT(ts *testscript.TestScript) TestingTS {
- return &testingTS{ts}
-}
-
-func (t *testingTS) Errorf(format string, args ...interface{}) {
- defer recover() // we can ignore recover result, we just want to catch it up
- t.Fatalf(format, args...)
-}
-
-func (t *testingTS) FailNow() {
- // unfortunately we can't access underlying `t.t.FailNow` method
- panic(errFailNow)
-}
diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go
deleted file mode 100644
index d525591f51e..00000000000
--- a/gno.land/pkg/integration/testing_integration.go
+++ /dev/null
@@ -1,731 +0,0 @@
-package integration
-
-import (
- "context"
- "errors"
- "fmt"
- "hash/crc32"
- "log/slog"
- "os"
- "path/filepath"
- "strconv"
- "strings"
- "testing"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
- "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"
- bft "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/crypto"
- "github.com/gnolang/gno/tm2/pkg/crypto/bip39"
- "github.com/gnolang/gno/tm2/pkg/crypto/keys"
- "github.com/gnolang/gno/tm2/pkg/crypto/keys/client"
- tm2Log "github.com/gnolang/gno/tm2/pkg/log"
- "github.com/gnolang/gno/tm2/pkg/std"
- "github.com/rogpeppe/go-internal/testscript"
- "go.uber.org/zap/zapcore"
-)
-
-const (
- envKeyGenesis int = iota
- envKeyLogger
- envKeyPkgsLoader
-)
-
-type tSeqShim struct{ *testing.T }
-
-// noop Parallel method allow us to run test sequentially
-func (tSeqShim) Parallel() {}
-
-func (t tSeqShim) Run(name string, f func(testscript.T)) {
- t.T.Run(name, func(t *testing.T) {
- f(tSeqShim{t})
- })
-}
-
-func (t tSeqShim) Verbose() bool {
- return testing.Verbose()
-}
-
-// RunGnolandTestscripts sets up and runs txtar integration tests for gnoland nodes.
-// It prepares an in-memory gnoland node and initializes the necessary environment and custom commands.
-// The function adapts the test setup for use with the testscript package, enabling
-// the execution of gnoland and gnokey commands within txtar scripts.
-//
-// Refer to package documentation in doc.go for more information on commands and example txtar scripts.
-func RunGnolandTestscripts(t *testing.T, txtarDir string) {
- t.Helper()
-
- p := setupGnolandTestScript(t, txtarDir)
- if deadline, ok := t.Deadline(); ok && p.Deadline.IsZero() {
- p.Deadline = deadline
- }
-
- testscript.RunT(tSeqShim{t}, p)
-}
-
-type testNode struct {
- *node.Node
- nGnoKeyExec uint // Counter for execution of gnokey.
-}
-
-func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params {
- t.Helper()
-
- tmpdir := t.TempDir()
-
- // `gnoRootDir` should point to the local location of the gno repository.
- // It serves as the gno equivalent of GOROOT.
- gnoRootDir := gnoenv.RootDir()
-
- // `gnoHomeDir` should be the local directory where gnokey stores keys.
- gnoHomeDir := filepath.Join(tmpdir, "gno")
-
- // Testscripts run concurrently by default, so we need to be prepared for that.
- nodes := map[string]*testNode{}
-
- updateScripts, _ := strconv.ParseBool(os.Getenv("UPDATE_SCRIPTS"))
- persistWorkDir, _ := strconv.ParseBool(os.Getenv("TESTWORK"))
- return testscript.Params{
- UpdateScripts: updateScripts,
- TestWork: persistWorkDir,
- Dir: txtarDir,
- Setup: func(env *testscript.Env) error {
- kb, err := keys.NewKeyBaseFromDir(gnoHomeDir)
- if err != nil {
- return err
- }
-
- // create sessions ID
- var sid string
- {
- works := env.Getenv("WORK")
- sum := crc32.ChecksumIEEE([]byte(works))
- sid = strconv.FormatUint(uint64(sum), 16)
- env.Setenv("SID", sid)
- }
-
- // setup logger
- var logger *slog.Logger
- {
- logger = tm2Log.NewNoopLogger()
- if persistWorkDir || os.Getenv("LOG_PATH_DIR") != "" {
- logname := fmt.Sprintf("txtar-gnoland-%s.log", sid)
- logger, err = getTestingLogger(env, logname)
- if err != nil {
- return fmt.Errorf("unable to setup logger: %w", err)
- }
- }
-
- env.Values[envKeyLogger] = logger
- }
-
- // Track new user balances added via the `adduser`
- // command and packages added with the `loadpkg` command.
- // This genesis will be use when node is started.
- genesis := &gnoland.GnoGenesisState{
- Balances: LoadDefaultGenesisBalanceFile(t, gnoRootDir),
- Txs: []std.Tx{},
- }
-
- // test1 must be created outside of the loop below because it is already included in genesis so
- // attempting to recreate results in it getting overwritten and breaking existing tests that
- // rely on its address being static.
- kb.CreateAccount(DefaultAccount_Name, DefaultAccount_Seed, "", "", 0, 0)
- env.Setenv("USER_SEED_"+DefaultAccount_Name, DefaultAccount_Seed)
- env.Setenv("USER_ADDR_"+DefaultAccount_Name, DefaultAccount_Address)
-
- env.Values[envKeyGenesis] = genesis
- env.Values[envKeyPkgsLoader] = newPkgsLoader()
-
- env.Setenv("GNOROOT", gnoRootDir)
- env.Setenv("GNOHOME", gnoHomeDir)
-
- return nil
- },
- Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
- "gnoland": func(ts *testscript.TestScript, neg bool, args []string) {
- if len(args) == 0 {
- tsValidateError(ts, "gnoland", neg, fmt.Errorf("syntax: gnoland [start|stop]"))
- return
- }
-
- logger := ts.Value(envKeyLogger).(*slog.Logger) // grab logger
- sid := getNodeSID(ts) // grab session id
-
- var cmd string
- cmd, args = args[0], args[1:]
-
- var err error
- switch cmd {
- case "start":
- if nodeIsRunning(nodes, sid) {
- err = fmt.Errorf("node already started")
- break
- }
-
- // get packages
- pkgs := ts.Value(envKeyPkgsLoader).(*pkgsLoader) // grab logger
- creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1
- defaultFee := std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000)))
- pkgsTxs, err := pkgs.LoadPackages(creator, defaultFee, nil)
- if err != nil {
- ts.Fatalf("unable to load packages txs: %s", err)
- }
-
- // Warp up `ts` so we can pass it to other testing method
- t := TSTestingT(ts)
-
- // Generate config and node
- cfg := TestingMinimalNodeConfig(t, gnoRootDir)
- genesis := ts.Value(envKeyGenesis).(*gnoland.GnoGenesisState)
- genesis.Txs = append(pkgsTxs, genesis.Txs...)
-
- // setup genesis state
- cfg.Genesis.AppState = *genesis
-
- n, remoteAddr := TestingInMemoryNode(t, logger, cfg)
-
- // Register cleanup
- nodes[sid] = &testNode{Node: n}
-
- // Add default environments
- ts.Setenv("RPC_ADDR", remoteAddr)
-
- fmt.Fprintln(ts.Stdout(), "node started successfully")
- case "stop":
- n, ok := nodes[sid]
- if !ok {
- err = fmt.Errorf("node not started cannot be stopped")
- break
- }
- if err = n.Stop(); err == nil {
- delete(nodes, sid)
-
- // Unset gnoland environments
- ts.Setenv("RPC_ADDR", "")
- fmt.Fprintln(ts.Stdout(), "node stopped successfully")
- }
- default:
- err = fmt.Errorf("invalid gnoland subcommand: %q", cmd)
- }
-
- tsValidateError(ts, "gnoland "+cmd, neg, err)
- },
- "gnokey": func(ts *testscript.TestScript, neg bool, args []string) {
- logger := ts.Value(envKeyLogger).(*slog.Logger) // grab logger
- sid := ts.Getenv("SID") // grab session id
-
- // Unquote args enclosed in `"` to correctly handle `\n` or similar escapes.
- args, err := unquote(args)
- if err != nil {
- tsValidateError(ts, "gnokey", neg, err)
- }
-
- // Setup IO command
- io := commands.NewTestIO()
- io.SetOut(commands.WriteNopCloser(ts.Stdout()))
- io.SetErr(commands.WriteNopCloser(ts.Stderr()))
- cmd := keyscli.NewRootCmd(io, client.DefaultBaseOptions)
-
- io.SetIn(strings.NewReader("\n")) // Inject empty password to stdin.
- defaultArgs := []string{
- "-home", gnoHomeDir,
- "-insecure-password-stdin=true", // There no use to not have this param by default.
- }
-
- if n, ok := nodes[sid]; ok {
- if raddr := n.Config().RPC.ListenAddress; raddr != "" {
- defaultArgs = append(defaultArgs, "-remote", raddr)
- }
-
- n.nGnoKeyExec++
- headerlog := fmt.Sprintf("%.02d!EXEC_GNOKEY", n.nGnoKeyExec)
-
- // Log the command inside gnoland logger, so we can better scope errors.
- logger.Info(headerlog, "args", strings.Join(args, " "))
- defer logger.Info(headerlog, "delimiter", "END")
- }
-
- // Inject default argument, if duplicate
- // arguments, it should be override by the ones
- // user provided.
- args = append(defaultArgs, args...)
-
- err = cmd.ParseAndRun(context.Background(), args)
- tsValidateError(ts, "gnokey", neg, err)
- },
- // 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"))
- return
- }
-
- if len(args) == 0 {
- ts.Fatalf("new user name required")
- }
-
- kb, err := keys.NewKeyBaseFromDir(gnoHomeDir)
- if err != nil {
- ts.Fatalf("unable to get keybase")
- }
-
- balance, err := createAccount(ts, kb, args[0])
- if err != nil {
- ts.Fatalf("error creating account %s: %s", args[0], err)
- }
-
- // Add balance to genesis
- genesis := ts.Value(envKeyGenesis).(*gnoland.GnoGenesisState)
- genesis.Balances = append(genesis.Balances, balance)
- },
- // adduserfrom commands must be executed before starting the node; it errors out otherwise.
- "adduserfrom": func(ts *testscript.TestScript, neg bool, args []string) {
- if nodeIsRunning(nodes, getNodeSID(ts)) {
- tsValidateError(ts, "adduserfrom", neg, errors.New("adduserfrom must be used before starting node"))
- return
- }
-
- var account, index uint64
- var err error
-
- switch len(args) {
- case 2:
- // expected user input
- // adduserfrom 'username 'menmonic'
- // no need to do anything
-
- case 4:
- // expected user input
- // adduserfrom 'username 'menmonic' 'account' 'index'
-
- // parse 'index' first, then fallghrough to `case 3` to parse 'account'
- index, err = strconv.ParseUint(args[3], 10, 32)
- if err != nil {
- ts.Fatalf("invalid index number %s", args[3])
- }
-
- fallthrough // parse 'account'
- case 3:
- // expected user input
- // adduserfrom 'username 'menmonic' 'account'
-
- account, err = strconv.ParseUint(args[2], 10, 32)
- if err != nil {
- ts.Fatalf("invalid account number %s", args[2])
- }
- default:
- ts.Fatalf("to create account from metadatas, user name and mnemonic are required ( account and index are optional )")
- }
-
- kb, err := keys.NewKeyBaseFromDir(gnoHomeDir)
- if err != nil {
- ts.Fatalf("unable to get keybase")
- }
-
- balance, err := createAccountFrom(ts, kb, args[0], args[1], uint32(account), uint32(index))
- if err != nil {
- ts.Fatalf("error creating wallet %s", err)
- }
-
- // Add balance to genesis
- genesis := ts.Value(envKeyGenesis).(*gnoland.GnoGenesisState)
- genesis.Balances = append(genesis.Balances, balance)
-
- fmt.Fprintf(ts.Stdout(), "Added %s(%s) to genesis", args[0], balance.Address)
- },
- // `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")
- examplesDir := filepath.Join(gnoRootDir, "examples")
-
- pkgs := ts.Value(envKeyPkgsLoader).(*pkgsLoader)
-
- var path, name string
- switch len(args) {
- case 2:
- name = args[0]
- path = filepath.Clean(args[1])
- case 1:
- path = filepath.Clean(args[0])
- case 0:
- ts.Fatalf("`loadpkg`: no arguments specified")
- default:
- ts.Fatalf("`loadpkg`: too many arguments specified")
- }
-
- // If `all` is specified, fully load 'examples' directory.
- // NOTE: In 99% of cases, this is not needed, and
- // packages should be loaded individually.
- if path == "all" {
- ts.Logf("warning: loading all packages")
- if err := pkgs.LoadAllPackagesFromDir(examplesDir); err != nil {
- ts.Fatalf("unable to load packages from %q: %s", examplesDir, err)
- }
-
- return
- }
-
- if !strings.HasPrefix(path, workDir) {
- path = filepath.Join(examplesDir, path)
- }
-
- if err := pkgs.LoadPackage(examplesDir, path, name); err != nil {
- ts.Fatalf("`loadpkg` unable to load package(s) from %q: %s", args[0], err)
- }
-
- ts.Logf("%q package was added to genesis", args[0])
- },
- },
- }
-}
-
-// `unquote` takes a slice of strings, resulting from splitting a string block by spaces, and
-// processes them. The function handles quoted phrases and escape characters within these strings.
-func unquote(args []string) ([]string, error) {
- const quote = '"'
-
- parts := []string{}
- var inQuote bool
-
- var part strings.Builder
- for _, arg := range args {
- var escaped bool
- for _, c := range arg {
- if escaped {
- // If the character is meant to be escaped, it is processed with Unquote.
- // We use `Unquote` here for two main reasons:
- // 1. It will validate that the escape sequence is correct
- // 2. It converts the escaped string to its corresponding raw character.
- // For example, "\\t" becomes '\t'.
- uc, err := strconv.Unquote(`"\` + string(c) + `"`)
- if err != nil {
- return nil, fmt.Errorf("unhandled escape sequence `\\%c`: %w", c, err)
- }
-
- part.WriteString(uc)
- escaped = false
- continue
- }
-
- // If we are inside a quoted string and encounter an escape character,
- // flag the next character as `escaped`
- if inQuote && c == '\\' {
- escaped = true
- continue
- }
-
- // Detect quote and toggle inQuote state
- if c == quote {
- inQuote = !inQuote
- continue
- }
-
- // Handle regular character
- part.WriteRune(c)
- }
-
- // If we're inside a quote, add a single space.
- // It reflects one or multiple spaces between args in the original string.
- if inQuote {
- part.WriteRune(' ')
- continue
- }
-
- // Finalize part, add to parts, and reset for next part
- parts = append(parts, part.String())
- part.Reset()
- }
-
- // Check if a quote is left open
- if inQuote {
- return nil, errors.New("unfinished quote")
- }
-
- return parts, nil
-}
-
-func getNodeSID(ts *testscript.TestScript) string {
- return ts.Getenv("SID")
-}
-
-func nodeIsRunning(nodes map[string]*testNode, sid string) bool {
- _, ok := nodes[sid]
- return ok
-}
-
-func getTestingLogger(env *testscript.Env, logname string) (*slog.Logger, error) {
- var path string
-
- if logdir := os.Getenv("LOG_PATH_DIR"); logdir != "" {
- if err := os.MkdirAll(logdir, 0o755); err != nil {
- return nil, fmt.Errorf("unable to make log directory %q", logdir)
- }
-
- var err error
- if path, err = filepath.Abs(filepath.Join(logdir, logname)); err != nil {
- return nil, fmt.Errorf("unable to get absolute path of logdir %q", logdir)
- }
- } else if workdir := env.Getenv("WORK"); workdir != "" {
- path = filepath.Join(workdir, logname)
- } else {
- return tm2Log.NewNoopLogger(), nil
- }
-
- f, err := os.Create(path)
- if err != nil {
- return nil, fmt.Errorf("unable to create log file %q: %w", path, err)
- }
-
- env.Defer(func() {
- if err := f.Close(); err != nil {
- panic(fmt.Errorf("unable to close log file %q: %w", path, err))
- }
- })
-
- // Initialize the logger
- logLevel, err := zapcore.ParseLevel(strings.ToLower(os.Getenv("LOG_LEVEL")))
- if err != nil {
- return nil, fmt.Errorf("unable to parse log level, %w", err)
- }
-
- // Build zap logger for testing
- zapLogger := log.NewZapTestingLogger(f, logLevel)
- env.Defer(func() { zapLogger.Sync() })
-
- env.T().Log("starting logger", path)
- return log.ZapLoggerToSlog(zapLogger), nil
-}
-
-func tsValidateError(ts *testscript.TestScript, cmd string, neg bool, err error) {
- if err != nil {
- fmt.Fprintf(ts.Stderr(), "%q error: %+v\n", cmd, err)
- if !neg {
- ts.Fatalf("unexpected %q command failure: %s", cmd, err)
- }
- } else {
- if neg {
- ts.Fatalf("unexpected %q command success", cmd)
- }
- }
-}
-
-type envSetter interface {
- Setenv(key, value string)
-}
-
-// createAccount creates a new account with the given name and adds it to the keybase.
-func createAccount(env envSetter, kb keys.Keybase, accountName string) (gnoland.Balance, error) {
- var balance gnoland.Balance
- entropy, err := bip39.NewEntropy(256)
- if err != nil {
- return balance, fmt.Errorf("error creating entropy: %w", err)
- }
-
- mnemonic, err := bip39.NewMnemonic(entropy)
- if err != nil {
- return balance, fmt.Errorf("error generating mnemonic: %w", err)
- }
-
- var keyInfo keys.Info
- if keyInfo, err = kb.CreateAccount(accountName, mnemonic, "", "", 0, 0); err != nil {
- return balance, fmt.Errorf("unable to create account: %w", err)
- }
-
- address := keyInfo.GetAddress()
- env.Setenv("USER_SEED_"+accountName, mnemonic)
- env.Setenv("USER_ADDR_"+accountName, address.String())
-
- return gnoland.Balance{
- Address: address,
- Amount: std.Coins{std.NewCoin(ugnot.Denom, 10e6)},
- }, nil
-}
-
-// createAccountFrom creates a new account with the given metadata and adds it to the keybase.
-func createAccountFrom(env envSetter, kb keys.Keybase, accountName, mnemonic string, account, index uint32) (gnoland.Balance, error) {
- var balance gnoland.Balance
-
- // check if mnemonic is valid
- if !bip39.IsMnemonicValid(mnemonic) {
- return balance, fmt.Errorf("invalid mnemonic")
- }
-
- keyInfo, err := kb.CreateAccount(accountName, mnemonic, "", "", account, index)
- if err != nil {
- return balance, fmt.Errorf("unable to create account: %w", err)
- }
-
- address := keyInfo.GetAddress()
- env.Setenv("USER_SEED_"+accountName, mnemonic)
- env.Setenv("USER_ADDR_"+accountName, address.String())
-
- return gnoland.Balance{
- Address: address,
- Amount: std.Coins{std.NewCoin(ugnot.Denom, 10e6)},
- }, nil
-}
-
-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),
- }
-}
-
-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 {
- return nil, fmt.Errorf("unable to sort packages: %w", err)
- }
-
- txs := make([]std.Tx, len(pkgslist))
- for i, pkg := range pkgslist {
- tx, err := gnoland.LoadPackage(pkg, creator, fee, deposit)
- 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
- }
-
- return txs, nil
-}
-
-func (pl *pkgsLoader) LoadAllPackagesFromDir(path string) error {
- // list all packages from target path
- pkgslist, err := gnomod.ListPkgs(path)
- if err != nil {
- return fmt.Errorf("listing gno packages: %w", err)
- }
-
- for _, pkg := range pkgslist {
- if !pl.exist(pkg) {
- pl.add(pkg)
- }
- }
-
- return nil
-}
-
-func (pl *pkgsLoader) LoadPackage(modroot string, path, name string) error {
- // Initialize a queue with the root package
- queue := []gnomod.Pkg{{Dir: path, Name: name}}
-
- for len(queue) > 0 {
- // Dequeue the first package
- currentPkg := queue[0]
- queue = queue[1:]
-
- if currentPkg.Dir == "" {
- return fmt.Errorf("no path specified for package")
- }
-
- if currentPkg.Name == "" {
- // Load `gno.mod` information
- gnoModPath := filepath.Join(currentPkg.Dir, "gno.mod")
- gm, err := gnomod.ParseGnoMod(gnoModPath)
- if err != nil {
- return fmt.Errorf("unable to load %q: %w", gnoModPath, err)
- }
- gm.Sanitize()
-
- // Override package info with mod infos
- currentPkg.Name = gm.Module.Mod.Path
- currentPkg.Draft = gm.Draft
- for _, req := range gm.Require {
- currentPkg.Requires = append(currentPkg.Requires, req.Mod.Path)
- }
- }
-
- if currentPkg.Draft {
- continue // Skip draft package
- }
-
- if pl.exist(currentPkg) {
- continue
- }
- pl.add(currentPkg)
-
- // Add requirements to the queue
- for _, pkgPath := range currentPkg.Requires {
- fullPath := filepath.Join(modroot, pkgPath)
- queue = append(queue, gnomod.Pkg{Dir: fullPath})
- }
- }
-
- return nil
-}
-
-func (pl *pkgsLoader) add(pkg gnomod.Pkg) {
- pl.visited[pkg.Name] = struct{}{}
- pl.pkgs = append(pl.pkgs, pkg)
-}
-
-func (pl *pkgsLoader) exist(pkg gnomod.Pkg) (ok bool) {
- _, ok = pl.visited[pkg.Name]
- return
-}
diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go
deleted file mode 100644
index f3baf55b0dd..00000000000
--- a/gno.land/pkg/integration/testing_node.go
+++ /dev/null
@@ -1,158 +0,0 @@
-package integration
-
-import (
- "log/slog"
- "path/filepath"
- "time"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland"
- "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
- abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
- tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config"
- "github.com/gnolang/gno/tm2/pkg/bft/node"
- bft "github.com/gnolang/gno/tm2/pkg/bft/types"
- "github.com/gnolang/gno/tm2/pkg/crypto"
- "github.com/gnolang/gno/tm2/pkg/std"
- "github.com/stretchr/testify/require"
-)
-
-const (
- DefaultAccount_Name = "test1"
- DefaultAccount_Address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"
- DefaultAccount_Seed = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast"
-)
-
-// TestingInMemoryNode initializes and starts an in-memory node for testing.
-// It returns the node instance and its RPC remote address.
-func TestingInMemoryNode(t TestingTS, logger *slog.Logger, config *gnoland.InMemoryNodeConfig) (*node.Node, string) {
- node, err := gnoland.NewInMemoryNode(logger, config)
- require.NoError(t, err)
-
- err = node.Start()
- require.NoError(t, err)
-
- select {
- case <-node.Ready():
- case <-time.After(time.Second * 10):
- require.FailNow(t, "timeout while waiting for the node to start")
- }
-
- return node, node.Config().RPC.ListenAddress
-}
-
-// 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, additionalTxs ...std.Tx) (*gnoland.InMemoryNodeConfig, bft.Address) {
- cfg := TestingMinimalNodeConfig(t, gnoroot)
-
- creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1
-
- balances := LoadDefaultGenesisBalanceFile(t, gnoroot)
- txs := []std.Tx{}
- txs = append(txs, LoadDefaultPackages(t, creator, gnoroot)...)
- txs = append(txs, additionalTxs...)
-
- cfg.Genesis.AppState = gnoland.GnoGenesisState{
- Balances: balances,
- Txs: txs,
- }
-
- return cfg, creator
-}
-
-// TestingMinimalNodeConfig constructs the default minimal in-memory node configuration for testing.
-func TestingMinimalNodeConfig(t TestingTS, gnoroot string) *gnoland.InMemoryNodeConfig {
- tmconfig := DefaultTestingTMConfig(gnoroot)
-
- // Create Mocked Identity
- pv := gnoland.NewMockedPrivValidator()
-
- // Generate genesis config
- genesis := DefaultTestingGenesisConfig(t, gnoroot, pv.GetPubKey(), tmconfig)
-
- return &gnoland.InMemoryNodeConfig{
- PrivValidator: pv,
- Genesis: genesis,
- TMConfig: tmconfig,
- GenesisTxHandler: gnoland.PanicOnFailingTxHandler,
- }
-}
-
-func DefaultTestingGenesisConfig(t TestingTS, gnoroot string, self crypto.PubKey, tmconfig *tmcfg.Config) *bft.GenesisDoc {
- return &bft.GenesisDoc{
- GenesisTime: time.Now(),
- ChainID: tmconfig.ChainID(),
- ConsensusParams: abci.ConsensusParams{
- Block: &abci.BlockParams{
- MaxTxBytes: 1_000_000, // 1MB,
- MaxDataBytes: 2_000_000, // 2MB,
- MaxGas: 100_000_000, // 100M gas
- TimeIotaMS: 100, // 100ms
- },
- },
- Validators: []bft.GenesisValidator{
- {
- Address: self.Address(),
- PubKey: self,
- Power: 10,
- Name: "self",
- },
- },
- AppState: gnoland.GnoGenesisState{
- Balances: []gnoland.Balance{
- {
- Address: crypto.MustAddressFromString(DefaultAccount_Address),
- Amount: std.MustParseCoins(ugnot.ValueString(10000000000000)),
- },
- },
- Txs: []std.Tx{},
- },
- }
-}
-
-// LoadDefaultPackages loads the default packages for testing using a given creator address and gnoroot directory.
-func LoadDefaultPackages(t TestingTS, creator bft.Address, gnoroot string) []std.Tx {
- examplesDir := filepath.Join(gnoroot, "examples")
-
- defaultFee := std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000)))
- txs, err := gnoland.LoadPackagesFromDir(examplesDir, creator, defaultFee)
- require.NoError(t, err)
-
- return txs
-}
-
-// LoadDefaultGenesisBalanceFile loads the default genesis balance file for testing.
-func LoadDefaultGenesisBalanceFile(t TestingTS, gnoroot string) []gnoland.Balance {
- balanceFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_balances.txt")
-
- genesisBalances, err := gnoland.LoadGenesisBalancesFile(balanceFile)
- require.NoError(t, err)
-
- return genesisBalances
-}
-
-// LoadDefaultGenesisTXsFile loads the default genesis transactions file for testing.
-func LoadDefaultGenesisTXsFile(t TestingTS, chainid string, gnoroot string) []std.Tx {
- txsFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_txs.jsonl")
-
- // NOTE: We dont care about giving a correct address here, as it's only for display
- // XXX: Do we care loading this TXs for testing ?
- genesisTXs, err := gnoland.LoadGenesisTxsFile(txsFile, chainid, "https://127.0.0.1:26657")
- require.NoError(t, err)
-
- return genesisTXs
-}
-
-// DefaultTestingTMConfig constructs the default Tendermint configuration for testing.
-func DefaultTestingTMConfig(gnoroot string) *tmcfg.Config {
- const defaultListner = "tcp://127.0.0.1:0"
-
- tmconfig := tmcfg.TestConfig().SetRootDir(gnoroot)
- tmconfig.Consensus.WALDisabled = true
- tmconfig.Consensus.CreateEmptyBlocks = true
- tmconfig.Consensus.CreateEmptyBlocksInterval = time.Duration(0)
- tmconfig.RPC.ListenAddress = defaultListner
- tmconfig.P2P.ListenAddress = defaultListner
- return tmconfig
-}
diff --git a/gno.land/pkg/integration/testscript_gnoland.go b/gno.land/pkg/integration/testscript_gnoland.go
new file mode 100644
index 00000000000..1531b83dfef
--- /dev/null
+++ b/gno.land/pkg/integration/testscript_gnoland.go
@@ -0,0 +1,782 @@
+package integration
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "flag"
+ "fmt"
+ "hash/crc32"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/gnolang/gno/gno.land/pkg/gnoland"
+ "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
+ "github.com/gnolang/gno/gno.land/pkg/keyscli"
+ "github.com/gnolang/gno/gnovm/pkg/gnoenv"
+ "github.com/gnolang/gno/tm2/pkg/amino"
+ rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client"
+ ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types"
+ bft "github.com/gnolang/gno/tm2/pkg/bft/types"
+ "github.com/gnolang/gno/tm2/pkg/commands"
+ "github.com/gnolang/gno/tm2/pkg/crypto"
+ "github.com/gnolang/gno/tm2/pkg/crypto/bip39"
+ "github.com/gnolang/gno/tm2/pkg/crypto/ed25519"
+ "github.com/gnolang/gno/tm2/pkg/crypto/hd"
+ "github.com/gnolang/gno/tm2/pkg/crypto/keys"
+ "github.com/gnolang/gno/tm2/pkg/crypto/keys/client"
+ "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1"
+ "github.com/gnolang/gno/tm2/pkg/std"
+ "github.com/rogpeppe/go-internal/testscript"
+ "github.com/stretchr/testify/require"
+)
+
+const nodeMaxLifespan = time.Second * 30
+
+type envKey int
+
+const (
+ envKeyGenesis envKey = iota
+ envKeyLogger
+ envKeyPkgsLoader
+ envKeyPrivValKey
+ envKeyExecCommand
+ envKeyExecBin
+ envKeyBase
+)
+
+type commandkind int
+
+const (
+ // commandKindBin builds and uses an integration binary to run the testscript
+ // in a separate process. This should be used for any external package that
+ // wants to use test scripts.
+ commandKindBin commandkind = iota
+ // commandKindTesting uses the current testing binary to run the testscript
+ // in a separate process. This command cannot be used outside this package.
+ commandKindTesting
+ // commandKindInMemory runs testscripts in memory.
+ commandKindInMemory
+)
+
+type tNodeProcess struct {
+ NodeProcess
+ cfg *gnoland.InMemoryNodeConfig
+ nGnoKeyExec uint // Counter for execution of gnokey.
+}
+
+// NodesManager manages access to the nodes map with synchronization.
+type NodesManager struct {
+ nodes map[string]*tNodeProcess
+ mu sync.RWMutex
+}
+
+// NewNodesManager creates a new instance of NodesManager.
+func NewNodesManager() *NodesManager {
+ return &NodesManager{
+ nodes: make(map[string]*tNodeProcess),
+ }
+}
+
+func (nm *NodesManager) IsNodeRunning(sid string) bool {
+ nm.mu.RLock()
+ defer nm.mu.RUnlock()
+
+ _, ok := nm.nodes[sid]
+ return ok
+}
+
+// Get retrieves a node by its SID.
+func (nm *NodesManager) Get(sid string) (*tNodeProcess, bool) {
+ nm.mu.RLock()
+ defer nm.mu.RUnlock()
+ node, exists := nm.nodes[sid]
+ return node, exists
+}
+
+// Set adds or updates a node in the map.
+func (nm *NodesManager) Set(sid string, node *tNodeProcess) {
+ nm.mu.Lock()
+ defer nm.mu.Unlock()
+ nm.nodes[sid] = node
+}
+
+// Delete removes a node from the map.
+func (nm *NodesManager) Delete(sid string) {
+ nm.mu.Lock()
+ defer nm.mu.Unlock()
+ delete(nm.nodes, sid)
+}
+
+func SetupGnolandTestscript(t *testing.T, p *testscript.Params) error {
+ t.Helper()
+
+ gnoRootDir := gnoenv.RootDir()
+
+ nodesManager := NewNodesManager()
+
+ defaultPK, err := GeneratePrivKeyFromMnemonic(DefaultAccount_Seed, "", 0, 0)
+ require.NoError(t, err)
+
+ var buildOnce sync.Once
+ var gnolandBin string
+
+ // Store the original setup scripts for potential wrapping
+ origSetup := p.Setup
+ p.Setup = func(env *testscript.Env) error {
+ // If there's an original setup, execute it
+ if origSetup != nil {
+ if err := origSetup(env); err != nil {
+ return err
+ }
+ }
+
+ cmd, isSet := env.Values[envKeyExecCommand].(commandkind)
+ switch {
+ case !isSet:
+ cmd = commandKindBin // fallback on commandKindBin
+ fallthrough
+ case cmd == commandKindBin:
+ buildOnce.Do(func() {
+ t.Logf("building the gnoland integration node")
+ start := time.Now()
+ gnolandBin = buildGnoland(t, gnoRootDir)
+ t.Logf("time to build the node: %v", time.Since(start).String())
+ })
+
+ env.Values[envKeyExecBin] = gnolandBin
+ }
+
+ tmpdir, dbdir := t.TempDir(), t.TempDir()
+ gnoHomeDir := filepath.Join(tmpdir, "gno")
+
+ kb, err := keys.NewKeyBaseFromDir(gnoHomeDir)
+ if err != nil {
+ return err
+ }
+
+ kb.ImportPrivKey(DefaultAccount_Name, defaultPK, "")
+ env.Setenv(DefaultAccount_Name+"_user_seed", DefaultAccount_Seed)
+ env.Setenv(DefaultAccount_Name+"_user_addr", DefaultAccount_Address)
+
+ // New private key
+ env.Values[envKeyPrivValKey] = ed25519.GenPrivKey()
+
+ // Set gno dbdir
+ env.Setenv("GNO_DBDIR", dbdir)
+
+ // Setup account store
+ env.Values[envKeyBase] = kb
+
+ // Generate node short id
+ var sid string
+ {
+ works := env.Getenv("WORK")
+ sum := crc32.ChecksumIEEE([]byte(works))
+ sid = strconv.FormatUint(uint64(sum), 16)
+ env.Setenv("SID", sid)
+ }
+
+ balanceFile := LoadDefaultGenesisBalanceFile(t, gnoRootDir)
+ genesisParamFile := LoadDefaultGenesisParamFile(t, gnoRootDir)
+
+ // Track new user balances added via the `adduser`
+ // command and packages added with the `loadpkg` command.
+ // This genesis will be use when node is started.
+ genesis := &gnoland.GnoGenesisState{
+ Balances: balanceFile,
+ Params: genesisParamFile,
+ Txs: []gnoland.TxWithMetadata{},
+ }
+
+ env.Values[envKeyGenesis] = genesis
+ env.Values[envKeyPkgsLoader] = NewPkgsLoader()
+
+ env.Setenv("GNOROOT", gnoRootDir)
+ env.Setenv("GNOHOME", gnoHomeDir)
+
+ env.Defer(func() {
+ // Gracefully stop the node, if any
+ n, exist := nodesManager.Get(sid)
+ if !exist {
+ return
+ }
+
+ if err := n.Stop(); err != nil {
+ err = fmt.Errorf("unable to stop the node gracefully: %w", err)
+ env.T().Fatal(err.Error())
+ }
+ })
+
+ return nil
+ }
+
+ cmds := map[string]func(ts *testscript.TestScript, neg bool, args []string){
+ "gnoland": gnolandCmd(t, nodesManager, gnoRootDir),
+ "gnokey": gnokeyCmd(nodesManager),
+ "adduser": adduserCmd(nodesManager),
+ "adduserfrom": adduserfromCmd(nodesManager),
+ "patchpkg": patchpkgCmd(),
+ "loadpkg": loadpkgCmd(gnoRootDir),
+ "scanf": loadpkgCmd(gnoRootDir),
+ }
+
+ // Initialize cmds map if needed
+ if p.Cmds == nil {
+ p.Cmds = make(map[string]func(ts *testscript.TestScript, neg bool, args []string))
+ }
+
+ // Register gnoland command
+ for cmd, call := range cmds {
+ if _, exist := p.Cmds[cmd]; exist {
+ panic(fmt.Errorf("unable register %q: command already exist", cmd))
+ }
+
+ p.Cmds[cmd] = call
+ }
+
+ return nil
+}
+
+func gnolandCmd(t *testing.T, nodesManager *NodesManager, gnoRootDir string) func(ts *testscript.TestScript, neg bool, args []string) {
+ t.Helper()
+
+ defaultPK, err := GeneratePrivKeyFromMnemonic(DefaultAccount_Seed, "", 0, 0)
+ require.NoError(t, err)
+
+ return func(ts *testscript.TestScript, neg bool, args []string) {
+ sid := getNodeSID(ts)
+
+ cmd, cmdargs := "", []string{}
+ if len(args) > 0 {
+ cmd, cmdargs = args[0], args[1:]
+ }
+
+ var err error
+ switch cmd {
+ case "":
+ err = errors.New("no command provided")
+ case "start":
+ if nodesManager.IsNodeRunning(sid) {
+ err = fmt.Errorf("node already started")
+ break
+ }
+
+ // XXX: this is a bit hacky, we should consider moving
+ // gnoland into his own package to be able to use it
+ // directly or use the config command for this.
+ fs := flag.NewFlagSet("start", flag.ContinueOnError)
+ nonVal := fs.Bool("non-validator", false, "set up node as a non-validator")
+ if err := fs.Parse(cmdargs); err != nil {
+ ts.Fatalf("unable to parse `gnoland start` flags: %s", err)
+ }
+
+ pkgs := ts.Value(envKeyPkgsLoader).(*PkgsLoader)
+ defaultFee := std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000)))
+ pkgsTxs, err := pkgs.LoadPackages(defaultPK, defaultFee, nil)
+ if err != nil {
+ ts.Fatalf("unable to load packages txs: %s", err)
+ }
+
+ cfg := TestingMinimalNodeConfig(gnoRootDir)
+ genesis := ts.Value(envKeyGenesis).(*gnoland.GnoGenesisState)
+ genesis.Txs = append(pkgsTxs, genesis.Txs...)
+
+ cfg.Genesis.AppState = *genesis
+ if *nonVal {
+ pv := gnoland.NewMockedPrivValidator()
+ cfg.Genesis.Validators = []bft.GenesisValidator{
+ {
+ Address: pv.GetPubKey().Address(),
+ PubKey: pv.GetPubKey(),
+ Power: 10,
+ Name: "none",
+ },
+ }
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), nodeMaxLifespan)
+ ts.Defer(cancel)
+
+ dbdir := ts.Getenv("GNO_DBDIR")
+ priv := ts.Value(envKeyPrivValKey).(ed25519.PrivKeyEd25519)
+ nodep := setupNode(ts, ctx, &ProcessNodeConfig{
+ ValidatorKey: priv,
+ DBDir: dbdir,
+ RootDir: gnoRootDir,
+ TMConfig: cfg.TMConfig,
+ Genesis: NewMarshalableGenesisDoc(cfg.Genesis),
+ })
+
+ nodesManager.Set(sid, &tNodeProcess{NodeProcess: nodep, cfg: cfg})
+ ts.Setenv("RPC_ADDR", nodep.Address())
+
+ // Load user infos
+ loadUserEnv(ts, nodep.Address())
+
+ fmt.Fprintln(ts.Stdout(), "node started successfully")
+
+ case "restart":
+ node, exists := nodesManager.Get(sid)
+ if !exists {
+ err = fmt.Errorf("node must be started before being restarted")
+ break
+ }
+
+ if err := node.Stop(); err != nil {
+ err = fmt.Errorf("unable to stop the node gracefully: %w", err)
+ break
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), nodeMaxLifespan)
+ ts.Defer(cancel)
+
+ priv := ts.Value(envKeyPrivValKey).(ed25519.PrivKeyEd25519)
+ dbdir := ts.Getenv("GNO_DBDIR")
+ nodep := setupNode(ts, ctx, &ProcessNodeConfig{
+ ValidatorKey: priv,
+ DBDir: dbdir,
+ RootDir: gnoRootDir,
+ TMConfig: node.cfg.TMConfig,
+ Genesis: NewMarshalableGenesisDoc(node.cfg.Genesis),
+ })
+
+ ts.Setenv("RPC_ADDR", nodep.Address())
+ nodesManager.Set(sid, &tNodeProcess{NodeProcess: nodep, cfg: node.cfg})
+
+ // Load user infos
+ loadUserEnv(ts, nodep.Address())
+
+ fmt.Fprintln(ts.Stdout(), "node restarted successfully")
+
+ case "stop":
+ node, exists := nodesManager.Get(sid)
+ if !exists {
+ err = fmt.Errorf("node not started cannot be stopped")
+ break
+ }
+
+ if err := node.Stop(); err != nil {
+ err = fmt.Errorf("unable to stop the node gracefully: %w", err)
+ break
+ }
+
+ fmt.Fprintln(ts.Stdout(), "node stopped successfully")
+ nodesManager.Delete(sid)
+
+ default:
+ err = fmt.Errorf("not supported command: %q", cmd)
+ // XXX: support gnoland other commands
+ }
+
+ tsValidateError(ts, strings.TrimSpace("gnoland "+cmd), neg, err)
+ }
+}
+
+func gnokeyCmd(nodes *NodesManager) func(ts *testscript.TestScript, neg bool, args []string) {
+ return func(ts *testscript.TestScript, neg bool, args []string) {
+ gnoHomeDir := ts.Getenv("GNOHOME")
+
+ sid := getNodeSID(ts)
+
+ args, err := unquote(args)
+ if err != nil {
+ tsValidateError(ts, "gnokey", neg, err)
+ }
+
+ io := commands.NewTestIO()
+ io.SetOut(commands.WriteNopCloser(ts.Stdout()))
+ io.SetErr(commands.WriteNopCloser(ts.Stderr()))
+ cmd := keyscli.NewRootCmd(io, client.DefaultBaseOptions)
+
+ io.SetIn(strings.NewReader("\n"))
+ defaultArgs := []string{
+ "-home", gnoHomeDir,
+ "-insecure-password-stdin=true",
+ }
+
+ if n, ok := nodes.Get(sid); ok {
+ if raddr := n.Address(); raddr != "" {
+ defaultArgs = append(defaultArgs, "-remote", raddr)
+ }
+
+ n.nGnoKeyExec++
+ }
+
+ args = append(defaultArgs, args...)
+
+ err = cmd.ParseAndRun(context.Background(), args)
+ tsValidateError(ts, "gnokey", neg, err)
+ }
+}
+
+func adduserCmd(nodesManager *NodesManager) func(ts *testscript.TestScript, neg bool, args []string) {
+ return func(ts *testscript.TestScript, neg bool, args []string) {
+ gnoHomeDir := ts.Getenv("GNOHOME")
+
+ sid := getNodeSID(ts)
+ if nodesManager.IsNodeRunning(sid) {
+ tsValidateError(ts, "adduser", neg, errors.New("adduser must be used before starting node"))
+ return
+ }
+
+ if len(args) == 0 {
+ ts.Fatalf("new user name required")
+ }
+
+ kb, err := keys.NewKeyBaseFromDir(gnoHomeDir)
+ if err != nil {
+ ts.Fatalf("unable to get keybase")
+ }
+
+ balance, err := createAccount(ts, kb, args[0])
+ if err != nil {
+ ts.Fatalf("error creating account %s: %s", args[0], err)
+ }
+
+ genesis := ts.Value(envKeyGenesis).(*gnoland.GnoGenesisState)
+ genesis.Balances = append(genesis.Balances, balance)
+ }
+}
+
+func adduserfromCmd(nodesManager *NodesManager) func(ts *testscript.TestScript, neg bool, args []string) {
+ return func(ts *testscript.TestScript, neg bool, args []string) {
+ gnoHomeDir := ts.Getenv("GNOHOME")
+
+ sid := getNodeSID(ts)
+ if nodesManager.IsNodeRunning(sid) {
+ tsValidateError(ts, "adduserfrom", neg, errors.New("adduserfrom must be used before starting node"))
+ return
+ }
+
+ var account, index uint64
+ var err error
+
+ switch len(args) {
+ case 2:
+ case 4:
+ index, err = strconv.ParseUint(args[3], 10, 32)
+ if err != nil {
+ ts.Fatalf("invalid index number %s", args[3])
+ }
+ fallthrough
+ case 3:
+ account, err = strconv.ParseUint(args[2], 10, 32)
+ if err != nil {
+ ts.Fatalf("invalid account number %s", args[2])
+ }
+ default:
+ ts.Fatalf("to create account from metadatas, user name and mnemonic are required ( account and index are optional )")
+ }
+
+ kb, err := keys.NewKeyBaseFromDir(gnoHomeDir)
+ if err != nil {
+ ts.Fatalf("unable to get keybase")
+ }
+
+ balance, err := createAccountFrom(ts, kb, args[0], args[1], uint32(account), uint32(index))
+ if err != nil {
+ ts.Fatalf("error creating wallet %s", err)
+ }
+
+ genesis := ts.Value(envKeyGenesis).(*gnoland.GnoGenesisState)
+ genesis.Balances = append(genesis.Balances, balance)
+
+ fmt.Fprintf(ts.Stdout(), "Added %s(%s) to genesis", args[0], balance.Address)
+ }
+}
+
+func patchpkgCmd() func(ts *testscript.TestScript, neg bool, args []string) {
+ return 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)
+ }
+}
+
+func loadpkgCmd(gnoRootDir string) func(ts *testscript.TestScript, neg bool, args []string) {
+ return func(ts *testscript.TestScript, neg bool, args []string) {
+ workDir := ts.Getenv("WORK")
+ examplesDir := filepath.Join(gnoRootDir, "examples")
+
+ pkgs := ts.Value(envKeyPkgsLoader).(*PkgsLoader)
+
+ var path, name string
+ switch len(args) {
+ case 2:
+ name = args[0]
+ path = filepath.Clean(args[1])
+ case 1:
+ path = filepath.Clean(args[0])
+ case 0:
+ ts.Fatalf("`loadpkg`: no arguments specified")
+ default:
+ ts.Fatalf("`loadpkg`: too many arguments specified")
+ }
+
+ if path == "all" {
+ ts.Logf("warning: loading all packages")
+ if err := pkgs.LoadAllPackagesFromDir(examplesDir); err != nil {
+ ts.Fatalf("unable to load packages from %q: %s", examplesDir, err)
+ }
+
+ return
+ }
+
+ if !strings.HasPrefix(path, workDir) {
+ path = filepath.Join(examplesDir, path)
+ }
+
+ if err := pkgs.LoadPackage(examplesDir, path, name); err != nil {
+ ts.Fatalf("`loadpkg` unable to load package(s) from %q: %s", args[0], err)
+ }
+
+ ts.Logf("%q package was added to genesis", args[0])
+ }
+}
+
+func loadUserEnv(ts *testscript.TestScript, remote string) error {
+ const path = "auth/accounts"
+
+ // List all accounts
+ kb := ts.Value(envKeyBase).(keys.Keybase)
+ accounts, err := kb.List()
+ if err != nil {
+ ts.Fatalf("query accounts: unable to list keys: %s", err)
+ }
+
+ cli, err := rpcclient.NewHTTPClient(remote)
+ if err != nil {
+ return fmt.Errorf("unable create rpc client %q: %w", remote, err)
+ }
+
+ batch := cli.NewBatch()
+ for _, account := range accounts {
+ accountPath := filepath.Join(path, account.GetAddress().String())
+ if err := batch.ABCIQuery(accountPath, []byte{}); err != nil {
+ return fmt.Errorf("unable to create query request: %w", err)
+ }
+ }
+
+ batchRes, err := batch.Send(context.Background())
+ if err != nil {
+ return fmt.Errorf("unable to query accounts: %w", err)
+ }
+
+ if len(batchRes) != len(accounts) {
+ ts.Fatalf("query accounts: len(res) != len(accounts)")
+ }
+
+ for i, res := range batchRes {
+ account := accounts[i]
+ name := account.GetName()
+ qres := res.(*ctypes.ResultABCIQuery)
+
+ if err := qres.Response.Error; err != nil {
+ ts.Fatalf("query account %q error: %s", account.GetName(), err.Error())
+ }
+
+ var qret struct{ BaseAccount std.BaseAccount }
+ if err = amino.UnmarshalJSON(qres.Response.Data, &qret); err != nil {
+ ts.Fatalf("query account %q unarmshal error: %s", account.GetName(), err.Error())
+ }
+
+ strAccountNumber := strconv.Itoa(int(qret.BaseAccount.GetAccountNumber()))
+ ts.Setenv(name+"_account_num", strAccountNumber)
+ ts.Logf("[%q] account number: %s", name, strAccountNumber)
+
+ strAccountSequence := strconv.Itoa(int(qret.BaseAccount.GetSequence()))
+ ts.Setenv(name+"_account_seq", strAccountSequence)
+ ts.Logf("[%q] account sequence: %s", name, strAccountNumber)
+ }
+
+ return nil
+}
+
+type tsLogWriter struct {
+ ts *testscript.TestScript
+}
+
+func (l *tsLogWriter) Write(p []byte) (n int, err error) {
+ l.ts.Logf(string(p))
+ return len(p), nil
+}
+
+func setupNode(ts *testscript.TestScript, ctx context.Context, cfg *ProcessNodeConfig) NodeProcess {
+ pcfg := ProcessConfig{
+ Node: cfg,
+ Stdout: &tsLogWriter{ts},
+ Stderr: ts.Stderr(),
+ }
+
+ // Setup coverdir provided
+ if coverdir := ts.Getenv("GOCOVERDIR"); coverdir != "" {
+ pcfg.CoverDir = coverdir
+ }
+
+ val := ts.Value(envKeyExecCommand)
+
+ switch cmd := val.(commandkind); cmd {
+ case commandKindInMemory:
+ nodep, err := RunInMemoryProcess(ctx, pcfg)
+ if err != nil {
+ ts.Fatalf("unable to start in memory node: %s", err)
+ }
+
+ return nodep
+
+ case commandKindTesting:
+ if !testing.Testing() {
+ ts.Fatalf("unable to invoke testing process while not testing")
+ }
+
+ return runTestingNodeProcess(&testingTS{ts}, ctx, pcfg)
+
+ case commandKindBin:
+ bin := ts.Value(envKeyExecBin).(string)
+ nodep, err := RunNodeProcess(ctx, pcfg, bin)
+ if err != nil {
+ ts.Fatalf("unable to start process node: %s", err)
+ }
+
+ return nodep
+
+ default:
+ ts.Fatalf("unknown command kind: %+v", cmd)
+ }
+
+ return nil
+}
+
+// createAccount creates a new account with the given name and adds it to the keybase.
+func createAccount(ts *testscript.TestScript, kb keys.Keybase, accountName string) (gnoland.Balance, error) {
+ var balance gnoland.Balance
+ entropy, err := bip39.NewEntropy(256)
+ if err != nil {
+ return balance, fmt.Errorf("error creating entropy: %w", err)
+ }
+
+ mnemonic, err := bip39.NewMnemonic(entropy)
+ if err != nil {
+ return balance, fmt.Errorf("error generating mnemonic: %w", err)
+ }
+
+ return createAccountFrom(ts, kb, accountName, mnemonic, 0, 0)
+}
+
+// createAccountFrom creates a new account with the given metadata and adds it to the keybase.
+func createAccountFrom(ts *testscript.TestScript, kb keys.Keybase, accountName, mnemonic string, account, index uint32) (gnoland.Balance, error) {
+ var balance gnoland.Balance
+
+ // check if mnemonic is valid
+ if !bip39.IsMnemonicValid(mnemonic) {
+ return balance, fmt.Errorf("invalid mnemonic")
+ }
+
+ keyInfo, err := kb.CreateAccount(accountName, mnemonic, "", "", account, index)
+ if err != nil {
+ return balance, fmt.Errorf("unable to create account: %w", err)
+ }
+
+ address := keyInfo.GetAddress()
+ ts.Setenv(accountName+"_user_seed", mnemonic)
+ ts.Setenv(accountName+"_user_addr", address.String())
+
+ return gnoland.Balance{
+ Address: address,
+ Amount: std.Coins{std.NewCoin(ugnot.Denom, 10e6)},
+ }, nil
+}
+
+func buildGnoland(t *testing.T, rootdir string) string {
+ t.Helper()
+
+ bin := filepath.Join(t.TempDir(), "gnoland-test")
+
+ t.Log("building gnoland integration binary...")
+
+ // Build a fresh gno binary in a temp directory
+ gnoArgsBuilder := []string{"build", "-o", bin}
+
+ os.Executable()
+
+ // Forward `-covermode` settings if set
+ if coverMode := testing.CoverMode(); coverMode != "" {
+ gnoArgsBuilder = append(gnoArgsBuilder,
+ "-covermode", coverMode,
+ )
+ }
+
+ // Append the path to the gno command source
+ gnoArgsBuilder = append(gnoArgsBuilder, filepath.Join(rootdir,
+ "gno.land", "pkg", "integration", "process"))
+
+ t.Logf("build command: %s", strings.Join(gnoArgsBuilder, " "))
+
+ cmd := exec.Command("go", gnoArgsBuilder...)
+
+ var buff bytes.Buffer
+ cmd.Stderr, cmd.Stdout = &buff, &buff
+ defer buff.Reset()
+
+ if err := cmd.Run(); err != nil {
+ require.FailNowf(t, "unable to build binary", "%q\n%s",
+ err.Error(), buff.String())
+ }
+
+ return bin
+}
+
+// GeneratePrivKeyFromMnemonic generates a crypto.PrivKey from a mnemonic.
+func GeneratePrivKeyFromMnemonic(mnemonic, bip39Passphrase string, account, index uint32) (crypto.PrivKey, error) {
+ // Generate Seed from Mnemonic
+ seed, err := bip39.NewSeedWithErrorChecking(mnemonic, bip39Passphrase)
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate seed: %w", err)
+ }
+
+ // Derive Private Key
+ coinType := crypto.CoinType // ensure this is set correctly in your context
+ hdPath := hd.NewFundraiserParams(account, coinType, index)
+ masterPriv, ch := hd.ComputeMastersFromSeed(seed)
+ derivedPriv, err := hd.DerivePrivateKeyForPath(masterPriv, ch, hdPath.String())
+ if err != nil {
+ return nil, fmt.Errorf("failed to derive private key: %w", err)
+ }
+
+ // Convert to secp256k1 private key
+ privKey := secp256k1.PrivKeySecp256k1(derivedPriv)
+ return privKey, nil
+}
+
+func getNodeSID(ts *testscript.TestScript) string {
+ return ts.Getenv("SID")
+}
+
+func tsValidateError(ts *testscript.TestScript, cmd string, neg bool, err error) {
+ if err != nil {
+ fmt.Fprintf(ts.Stderr(), "%q error: %+v\n", cmd, err)
+ if !neg {
+ ts.Fatalf("unexpected %q command failure: %s", cmd, err)
+ }
+ } else {
+ if neg {
+ ts.Fatalf("unexpected %q command success", cmd)
+ }
+ }
+}
diff --git a/gno.land/pkg/integration/testscript_testing.go b/gno.land/pkg/integration/testscript_testing.go
new file mode 100644
index 00000000000..9eed180dd8b
--- /dev/null
+++ b/gno.land/pkg/integration/testscript_testing.go
@@ -0,0 +1,41 @@
+package integration
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/rogpeppe/go-internal/testscript"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// This error is from testscript.Fatalf and is needed to correctly
+// handle the FailNow method.
+// see: https://github.com/rogpeppe/go-internal/blob/32ae33786eccde1672d4ba373c80e1bc282bfbf6/testscript/testscript.go#L799-L812
+var errFailNow = errors.New("fail now!") //nolint:stylecheck
+
+var (
+ _ require.TestingT = (*testingTS)(nil)
+ _ assert.TestingT = (*testingTS)(nil)
+ _ TestingTS = &testing.T{}
+)
+
+type TestingTS = require.TestingT
+
+type testingTS struct {
+ *testscript.TestScript
+}
+
+func TSTestingT(ts *testscript.TestScript) TestingTS {
+ return &testingTS{ts}
+}
+
+func (t *testingTS) Errorf(format string, args ...interface{}) {
+ defer recover() // we can ignore recover result, we just want to catch it up
+ t.Fatalf(format, args...)
+}
+
+func (t *testingTS) FailNow() {
+ // unfortunately we can't access underlying `t.t.FailNow` method
+ panic(errFailNow)
+}
diff --git a/gno.land/pkg/integration/utils.go b/gno.land/pkg/integration/utils.go
new file mode 100644
index 00000000000..bc9e7f1e220
--- /dev/null
+++ b/gno.land/pkg/integration/utils.go
@@ -0,0 +1,73 @@
+package integration
+
+import (
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+// `unquote` takes a slice of strings, resulting from splitting a string block by spaces, and
+// processes them. The function handles quoted phrases and escape characters within these strings.
+func unquote(args []string) ([]string, error) {
+ const quote = '"'
+
+ parts := []string{}
+ var inQuote bool
+
+ var part strings.Builder
+ for _, arg := range args {
+ var escaped bool
+ for _, c := range arg {
+ if escaped {
+ // If the character is meant to be escaped, it is processed with Unquote.
+ // We use `Unquote` here for two main reasons:
+ // 1. It will validate that the escape sequence is correct
+ // 2. It converts the escaped string to its corresponding raw character.
+ // For example, "\\t" becomes '\t'.
+ uc, err := strconv.Unquote(`"\` + string(c) + `"`)
+ if err != nil {
+ return nil, fmt.Errorf("unhandled escape sequence `\\%c`: %w", c, err)
+ }
+
+ part.WriteString(uc)
+ escaped = false
+ continue
+ }
+
+ // If we are inside a quoted string and encounter an escape character,
+ // flag the next character as `escaped`
+ if inQuote && c == '\\' {
+ escaped = true
+ continue
+ }
+
+ // Detect quote and toggle inQuote state
+ if c == quote {
+ inQuote = !inQuote
+ continue
+ }
+
+ // Handle regular character
+ part.WriteRune(c)
+ }
+
+ // If we're inside a quote, add a single space.
+ // It reflects one or multiple spaces between args in the original string.
+ if inQuote {
+ part.WriteRune(' ')
+ continue
+ }
+
+ // Finalize part, add to parts, and reset for next part
+ parts = append(parts, part.String())
+ part.Reset()
+ }
+
+ // Check if a quote is left open
+ if inQuote {
+ return nil, errors.New("unfinished quote")
+ }
+
+ return parts, nil
+}
diff --git a/gno.land/pkg/integration/utils_test.go b/gno.land/pkg/integration/utils_test.go
new file mode 100644
index 00000000000..2c301064969
--- /dev/null
+++ b/gno.land/pkg/integration/utils_test.go
@@ -0,0 +1,51 @@
+package integration
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestUnquote(t *testing.T) {
+ t.Parallel()
+
+ cases := []struct {
+ Input string
+ Expected []string
+ ShouldFail bool
+ }{
+ {"", []string{""}, false},
+ {"g", []string{"g"}, false},
+ {"Hello Gno", []string{"Hello", "Gno"}, false},
+ {`"Hello" "Gno"`, []string{"Hello", "Gno"}, false},
+ {`"Hel lo" "Gno"`, []string{"Hel lo", "Gno"}, false},
+ {`"H e l l o\n" \nGno`, []string{"H e l l o\n", "\\nGno"}, false},
+ {`"Hel\n"\nlo " ""G"n"o"`, []string{"Hel\n\\nlo", " Gno"}, false},
+ {`"He said, \"Hello\"" "Gno"`, []string{`He said, "Hello"`, "Gno"}, false},
+ {`"\n \t" \n\t`, []string{"\n \t", "\\n\\t"}, false},
+ {`"Hel\\n"\t\\nlo " ""\\nGno"`, []string{"Hel\\n\\t\\\\nlo", " \\nGno"}, false},
+ // errors:
+ {`"Hello Gno`, []string{}, true}, // unfinished quote
+ {`"Hello\e Gno"`, []string{}, true}, // unhandled escape sequence
+ }
+
+ for _, tc := range cases {
+ tc := tc
+ t.Run(tc.Input, func(t *testing.T) {
+ t.Parallel()
+
+ // split by whitespace to simulate command-line arguments
+ args := strings.Split(tc.Input, " ")
+ unquotedArgs, err := unquote(args)
+ if tc.ShouldFail {
+ require.Error(t, err)
+ return
+ }
+
+ require.NoError(t, err)
+ assert.Equal(t, tc.Expected, unquotedArgs)
+ })
+ }
+}
diff --git a/gno.land/pkg/keyscli/addpkg.go b/gno.land/pkg/keyscli/addpkg.go
index 37463d13b5c..5308d9d2ac4 100644
--- a/gno.land/pkg/keyscli/addpkg.go
+++ b/gno.land/pkg/keyscli/addpkg.go
@@ -71,6 +71,12 @@ func execMakeAddPkg(cfg *MakeAddPkgCfg, args []string, io commands.IO) error {
if cfg.PkgDir == "" {
return errors.New("pkgdir not specified")
}
+ if cfg.RootCfg.GasWanted == 0 {
+ return errors.New("gas-wanted not specified")
+ }
+ if cfg.RootCfg.GasFee == "" {
+ return errors.New("gas-fee not specified")
+ }
if len(args) != 1 {
return flag.ErrHelp
@@ -96,7 +102,7 @@ func execMakeAddPkg(cfg *MakeAddPkgCfg, args []string, io commands.IO) error {
}
// open files in directory as MemPackage.
- memPkg := gno.ReadMemPackage(cfg.PkgDir, cfg.PkgPath)
+ memPkg := gno.MustReadMemPackage(cfg.PkgDir, cfg.PkgPath)
if memPkg.IsEmpty() {
panic(fmt.Sprintf("found an empty package %q", cfg.PkgPath))
}
diff --git a/gno.land/pkg/keyscli/root.go b/gno.land/pkg/keyscli/root.go
index 19513fc0de6..c910e01b82c 100644
--- a/gno.land/pkg/keyscli/root.go
+++ b/gno.land/pkg/keyscli/root.go
@@ -30,6 +30,7 @@ func NewRootCmd(io commands.IO, base client.BaseOptions) *commands.Command {
cmd.AddSubCommands(
client.NewAddCmd(cfg, io),
client.NewDeleteCmd(cfg, io),
+ client.NewRotateCmd(cfg, io),
client.NewGenerateCmd(cfg, io),
client.NewExportCmd(cfg, io),
client.NewImportCmd(cfg, io),
diff --git a/gno.land/pkg/keyscli/run.go b/gno.land/pkg/keyscli/run.go
index aa0ee298201..00b2be585c6 100644
--- a/gno.land/pkg/keyscli/run.go
+++ b/gno.land/pkg/keyscli/run.go
@@ -8,6 +8,7 @@ import (
"os"
"github.com/gnolang/gno/gno.land/pkg/sdk/vm"
+ "github.com/gnolang/gno/gnovm"
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
"github.com/gnolang/gno/tm2/pkg/amino"
"github.com/gnolang/gno/tm2/pkg/commands"
@@ -73,13 +74,13 @@ func execMakeRun(cfg *MakeRunCfg, args []string, cmdio commands.IO) error {
return errors.Wrap(err, "parsing gas fee coin")
}
- memPkg := &std.MemPackage{}
+ memPkg := &gnovm.MemPackage{}
if sourcePath == "-" { // stdin
data, err := io.ReadAll(cmdio.In())
if err != nil {
return fmt.Errorf("could not read stdin: %w", err)
}
- memPkg.Files = []*std.MemFile{
+ memPkg.Files = []*gnovm.MemFile{
{
Name: "stdin.gno",
Body: string(data),
@@ -91,13 +92,13 @@ func execMakeRun(cfg *MakeRunCfg, args []string, cmdio commands.IO) error {
return fmt.Errorf("could not read source path: %q, %w", sourcePath, err)
}
if info.IsDir() {
- memPkg = gno.ReadMemPackage(sourcePath, "")
+ memPkg = gno.MustReadMemPackage(sourcePath, "")
} else { // is file
b, err := os.ReadFile(sourcePath)
if err != nil {
return fmt.Errorf("could not read %q: %w", sourcePath, err)
}
- memPkg.Files = []*std.MemFile{
+ memPkg.Files = []*gnovm.MemFile{
{
Name: info.Name(),
Body: string(b),
diff --git a/gno.land/pkg/log/zap.go b/gno.land/pkg/log/zap.go
index 38a9f13e4fc..cd5f6471546 100644
--- a/gno.land/pkg/log/zap.go
+++ b/gno.land/pkg/log/zap.go
@@ -71,5 +71,5 @@ func NewZapLogger(enc zapcore.Encoder, w io.Writer, level zapcore.Level, opts ..
// ZapLoggerToSlog wraps the given zap logger to an log/slog Logger
func ZapLoggerToSlog(logger *zap.Logger) *slog.Logger {
- return slog.New(zapslog.NewHandler(logger.Core(), nil))
+ return slog.New(zapslog.NewHandler(logger.Core()))
}
diff --git a/gno.land/pkg/sdk/vm/builtins.go b/gno.land/pkg/sdk/vm/builtins.go
index de58cd3e8ae..161e459873d 100644
--- a/gno.land/pkg/sdk/vm/builtins.go
+++ b/gno.land/pkg/sdk/vm/builtins.go
@@ -42,7 +42,7 @@ func (bnk *SDKBanker) TotalCoin(denom string) int64 {
func (bnk *SDKBanker) IssueCoin(b32addr crypto.Bech32Address, denom string, amount int64) {
addr := crypto.MustAddressFromString(string(b32addr))
- _, err := bnk.vmk.bank.AddCoins(bnk.ctx, addr, std.Coins{std.Coin{denom, amount}})
+ _, err := bnk.vmk.bank.AddCoins(bnk.ctx, addr, std.Coins{std.Coin{Denom: denom, Amount: amount}})
if err != nil {
panic(err)
}
@@ -50,8 +50,31 @@ func (bnk *SDKBanker) IssueCoin(b32addr crypto.Bech32Address, denom string, amou
func (bnk *SDKBanker) RemoveCoin(b32addr crypto.Bech32Address, denom string, amount int64) {
addr := crypto.MustAddressFromString(string(b32addr))
- _, err := bnk.vmk.bank.SubtractCoins(bnk.ctx, addr, std.Coins{std.Coin{denom, amount}})
+ _, err := bnk.vmk.bank.SubtractCoins(bnk.ctx, addr, std.Coins{std.Coin{Denom: denom, Amount: amount}})
if err != nil {
panic(err)
}
}
+
+// ----------------------------------------
+// SDKParams
+
+type SDKParams struct {
+ vmk *VMKeeper
+ ctx sdk.Context
+}
+
+func NewSDKParams(vmk *VMKeeper, ctx sdk.Context) *SDKParams {
+ return &SDKParams{
+ vmk: vmk,
+ ctx: ctx,
+ }
+}
+
+func (prm *SDKParams) SetString(key, value string) { prm.vmk.prmk.SetString(prm.ctx, key, value) }
+func (prm *SDKParams) SetBool(key string, value bool) { prm.vmk.prmk.SetBool(prm.ctx, key, value) }
+func (prm *SDKParams) SetInt64(key string, value int64) { prm.vmk.prmk.SetInt64(prm.ctx, key, value) }
+func (prm *SDKParams) SetUint64(key string, value uint64) {
+ prm.vmk.prmk.SetUint64(prm.ctx, key, value)
+}
+func (prm *SDKParams) SetBytes(key string, value []byte) { prm.vmk.prmk.SetBytes(prm.ctx, key, value) }
diff --git a/gno.land/pkg/sdk/vm/common_test.go b/gno.land/pkg/sdk/vm/common_test.go
index 6dd8050d6b6..10402f31f64 100644
--- a/gno.land/pkg/sdk/vm/common_test.go
+++ b/gno.land/pkg/sdk/vm/common_test.go
@@ -11,6 +11,7 @@ import (
"github.com/gnolang/gno/tm2/pkg/sdk"
authm "github.com/gnolang/gno/tm2/pkg/sdk/auth"
bankm "github.com/gnolang/gno/tm2/pkg/sdk/bank"
+ paramsm "github.com/gnolang/gno/tm2/pkg/sdk/params"
"github.com/gnolang/gno/tm2/pkg/std"
"github.com/gnolang/gno/tm2/pkg/store"
"github.com/gnolang/gno/tm2/pkg/store/dbadapter"
@@ -22,6 +23,7 @@ type testEnv struct {
vmk *VMKeeper
bank bankm.BankKeeper
acck authm.AccountKeeper
+ vmh vmHandler
}
func setupTestEnv() testEnv {
@@ -38,20 +40,31 @@ func _setupTestEnv(cacheStdlibs bool) testEnv {
baseCapKey := store.NewStoreKey("baseCapKey")
iavlCapKey := store.NewStoreKey("iavlCapKey")
+ // Mount db store and iavlstore
ms := store.NewCommitMultiStore(db)
ms.MountStoreWithDB(baseCapKey, dbadapter.StoreConstructor, db)
ms.MountStoreWithDB(iavlCapKey, iavl.StoreConstructor, db)
ms.LoadLatestVersion()
ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger())
- acck := authm.NewAccountKeeper(iavlCapKey, std.ProtoBaseAccount)
+ prmk := paramsm.NewParamsKeeper(iavlCapKey, "params")
+ acck := authm.NewAccountKeeper(iavlCapKey, prmk, std.ProtoBaseAccount)
bank := bankm.NewBankKeeper(acck)
- stdlibsDir := filepath.Join("..", "..", "..", "..", "gnovm", "stdlibs")
- vmk := NewVMKeeper(baseCapKey, iavlCapKey, acck, bank, stdlibsDir, 100_000_000)
+
+ vmk := NewVMKeeper(baseCapKey, iavlCapKey, acck, bank, prmk)
mcw := ms.MultiCacheWrap()
- vmk.Initialize(log.NewNoopLogger(), mcw, cacheStdlibs)
+ vmk.Initialize(log.NewNoopLogger(), mcw)
+ stdlibCtx := vmk.MakeGnoTransactionStore(ctx.WithMultiStore(mcw))
+ stdlibsDir := filepath.Join("..", "..", "..", "..", "gnovm", "stdlibs")
+ if cacheStdlibs {
+ vmk.LoadStdlibCached(stdlibCtx, stdlibsDir)
+ } else {
+ vmk.LoadStdlib(stdlibCtx, stdlibsDir)
+ }
+ vmk.CommitGnoTransactionStore(stdlibCtx)
mcw.MultiWrite()
+ vmh := NewHandler(vmk)
- return testEnv{ctx: ctx, vmk: vmk, bank: bank, acck: acck}
+ return testEnv{ctx: ctx, vmk: vmk, bank: bank, acck: acck, vmh: vmh}
}
diff --git a/gno.land/pkg/sdk/vm/convert.go b/gno.land/pkg/sdk/vm/convert.go
index cafb6cad67f..dbaabcfbc4b 100644
--- a/gno.land/pkg/sdk/vm/convert.go
+++ b/gno.land/pkg/sdk/vm/convert.go
@@ -3,6 +3,7 @@ package vm
import (
"encoding/base64"
"fmt"
+ "math"
"strconv"
"strings"
@@ -143,11 +144,11 @@ func convertArgToGno(arg string, argT gno.Type) (tv gno.TypedValue) {
return
case gno.Float32Type:
value := convertFloat(arg, 32)
- tv.SetFloat32(float32(value))
+ tv.SetFloat32(math.Float32bits(float32(value)))
return
case gno.Float64Type:
value := convertFloat(arg, 64)
- tv.SetFloat64(value)
+ tv.SetFloat64(math.Float64bits(value))
return
default:
panic(fmt.Sprintf("unexpected primitive type %s", bt.String()))
diff --git a/gno.land/pkg/sdk/vm/errors.go b/gno.land/pkg/sdk/vm/errors.go
index a0e71e08d14..c8d6da98970 100644
--- a/gno.land/pkg/sdk/vm/errors.go
+++ b/gno.land/pkg/sdk/vm/errors.go
@@ -16,6 +16,7 @@ func (abciError) AssertABCIError() {}
// NOTE: these are meant to be used in conjunction with pkgs/errors.
type (
InvalidPkgPathError struct{ abciError }
+ PkgExistError struct{ abciError }
InvalidStmtError struct{ abciError }
InvalidExprError struct{ abciError }
UnauthorizedUserError struct{ abciError }
@@ -26,6 +27,7 @@ type (
)
func (e InvalidPkgPathError) Error() string { return "invalid package path" }
+func (e PkgExistError) Error() string { return "package already exists" }
func (e InvalidStmtError) Error() string { return "invalid statement" }
func (e InvalidExprError) Error() string { return "invalid expression" }
func (e UnauthorizedUserError) Error() string { return "unauthorized user" }
@@ -36,6 +38,10 @@ func (e TypeCheckError) Error() string {
return bld.String()
}
+func ErrPkgAlreadyExists(msg string) error {
+ return errors.Wrap(PkgExistError{}, msg)
+}
+
func ErrUnauthorizedUser(msg string) error {
return errors.Wrap(UnauthorizedUserError{}, msg)
}
diff --git a/gno.land/pkg/sdk/vm/gas_test.go b/gno.land/pkg/sdk/vm/gas_test.go
index de647c8735a..acde3d315c6 100644
--- a/gno.land/pkg/sdk/vm/gas_test.go
+++ b/gno.land/pkg/sdk/vm/gas_test.go
@@ -4,6 +4,7 @@ import (
"testing"
"github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
+ "github.com/gnolang/gno/gnovm"
bft "github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/gnolang/gno/tm2/pkg/crypto"
"github.com/gnolang/gno/tm2/pkg/sdk"
@@ -20,13 +21,16 @@ import (
// Insufficient gas for a successful message.
func TestAddPkgDeliverTxInsuffGas(t *testing.T) {
- success := true
- ctx, tx, vmHandler := setupAddPkg(success)
+ isValidTx := true
+ ctx, tx, vmHandler := setupAddPkg(isValidTx)
ctx = ctx.WithMode(sdk.RunTxModeDeliver)
simulate := false
tx.Fee.GasWanted = 3000
gctx := auth.SetGasMeter(simulate, ctx, tx.Fee.GasWanted)
+ // Has to be set up after gas meter in the context; so the stores are
+ // correctly wrapped in gas stores.
+ gctx = vmHandler.vm.MakeGnoTransactionStore(gctx)
var res sdk.Result
abort := false
@@ -34,7 +38,7 @@ func TestAddPkgDeliverTxInsuffGas(t *testing.T) {
defer func() {
if r := recover(); r != nil {
switch r.(type) {
- case store.OutOfGasException:
+ case store.OutOfGasError:
res.Error = sdk.ABCIError(std.ErrOutOfGas(""))
abort = true
default:
@@ -43,7 +47,7 @@ func TestAddPkgDeliverTxInsuffGas(t *testing.T) {
assert.True(t, abort)
assert.False(t, res.IsOK())
gasCheck := gctx.GasMeter().GasConsumed()
- assert.Equal(t, int64(3231), gasCheck)
+ assert.Equal(t, int64(3462), gasCheck)
} else {
t.Errorf("should panic")
}
@@ -54,8 +58,8 @@ func TestAddPkgDeliverTxInsuffGas(t *testing.T) {
// Enough gas for a successful message.
func TestAddPkgDeliverTx(t *testing.T) {
- success := true
- ctx, tx, vmHandler := setupAddPkg(success)
+ isValidTx := true
+ ctx, tx, vmHandler := setupAddPkg(isValidTx)
var simulate bool
@@ -63,20 +67,21 @@ func TestAddPkgDeliverTx(t *testing.T) {
simulate = false
tx.Fee.GasWanted = 500000
gctx := auth.SetGasMeter(simulate, ctx, tx.Fee.GasWanted)
+ gctx = vmHandler.vm.MakeGnoTransactionStore(gctx)
msgs := tx.GetMsgs()
res := vmHandler.Process(gctx, msgs[0])
gasDeliver := gctx.GasMeter().GasConsumed()
assert.True(t, res.IsOK())
- // NOTE: let's try to keep this bellow 100_000 :)
- assert.Equal(t, int64(92825), gasDeliver)
+ // NOTE: let's try to keep this bellow 150_000 :)
+ assert.Equal(t, int64(143845), gasDeliver)
}
// Enough gas for a failed transaction.
func TestAddPkgDeliverTxFailed(t *testing.T) {
- success := false
- ctx, tx, vmHandler := setupAddPkg(success)
+ isValidTx := false
+ ctx, tx, vmHandler := setupAddPkg(isValidTx)
var simulate bool
@@ -84,25 +89,27 @@ func TestAddPkgDeliverTxFailed(t *testing.T) {
simulate = false
tx.Fee.GasWanted = 500000
gctx := auth.SetGasMeter(simulate, ctx, tx.Fee.GasWanted)
+ gctx = vmHandler.vm.MakeGnoTransactionStore(gctx)
msgs := tx.GetMsgs()
res := vmHandler.Process(gctx, msgs[0])
gasDeliver := gctx.GasMeter().GasConsumed()
assert.False(t, res.IsOK())
- assert.Equal(t, int64(2231), gasDeliver)
+ assert.Equal(t, int64(1231), gasDeliver)
}
// Not enough gas for a failed transaction.
func TestAddPkgDeliverTxFailedNoGas(t *testing.T) {
- success := false
- ctx, tx, vmHandler := setupAddPkg(success)
+ isValidTx := false
+ ctx, tx, vmHandler := setupAddPkg(isValidTx)
var simulate bool
ctx = ctx.WithMode(sdk.RunTxModeDeliver)
simulate = false
- tx.Fee.GasWanted = 2230
+ tx.Fee.GasWanted = 1230
gctx := auth.SetGasMeter(simulate, ctx, tx.Fee.GasWanted)
+ gctx = vmHandler.vm.MakeGnoTransactionStore(gctx)
var res sdk.Result
abort := false
@@ -110,7 +117,7 @@ func TestAddPkgDeliverTxFailedNoGas(t *testing.T) {
defer func() {
if r := recover(); r != nil {
switch r.(type) {
- case store.OutOfGasException:
+ case store.OutOfGasError:
res.Error = sdk.ABCIError(std.ErrOutOfGas(""))
abort = true
default:
@@ -119,7 +126,7 @@ func TestAddPkgDeliverTxFailedNoGas(t *testing.T) {
assert.True(t, abort)
assert.False(t, res.IsOK())
gasCheck := gctx.GasMeter().GasConsumed()
- assert.Equal(t, int64(2231), gasCheck)
+ assert.Equal(t, int64(1231), gasCheck)
} else {
t.Errorf("should panic")
}
@@ -129,23 +136,22 @@ func TestAddPkgDeliverTxFailedNoGas(t *testing.T) {
res = vmHandler.Process(gctx, msgs[0])
}
-// Set up a test env for both a successful and a failed tx
+// Set up a test env for both a successful and a failed tx.
func setupAddPkg(success bool) (sdk.Context, sdk.Tx, vmHandler) {
// setup
env := setupTestEnv()
ctx := env.ctx
// conduct base gas meter tests from a non-genesis block since genesis block use infinite gas meter instead.
ctx = ctx.WithBlockHeader(&bft.Header{Height: int64(1)})
- vmHandler := NewHandler(env.vmk)
// Create an account with 10M ugnot (10gnot)
addr := crypto.AddressFromPreimage([]byte("test1"))
acc := env.acck.NewAccountWithAddress(ctx, addr)
env.acck.SetAccount(ctx, acc)
env.bank.SetCoins(ctx, addr, std.MustParseCoins(ugnot.ValueString(10000000)))
// success message
- var files []*std.MemFile
+ var files []*gnovm.MemFile
if success {
- files = []*std.MemFile{
+ files = []*gnovm.MemFile{
{
Name: "hello.gno",
Body: `package hello
@@ -157,7 +163,7 @@ func Echo() string {
}
} else {
// failed message
- files = []*std.MemFile{
+ files = []*gnovm.MemFile{
{
Name: "hello.gno",
Body: `package hello
@@ -176,5 +182,5 @@ func Echo() UnknowType {
fee := std.NewFee(500000, std.MustParseCoin(ugnot.ValueString(1)))
tx := std.NewTx(msgs, fee, []std.Signature{}, "")
- return ctx, tx, vmHandler
+ return ctx, tx, env.vmh
}
diff --git a/gno.land/pkg/sdk/vm/handler.go b/gno.land/pkg/sdk/vm/handler.go
index 7b26265f35d..c484e07e887 100644
--- a/gno.land/pkg/sdk/vm/handler.go
+++ b/gno.land/pkg/sdk/vm/handler.go
@@ -1,17 +1,12 @@
package vm
import (
- "context"
"fmt"
"strings"
abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
"github.com/gnolang/gno/tm2/pkg/sdk"
"github.com/gnolang/gno/tm2/pkg/std"
- "github.com/gnolang/gno/tm2/pkg/telemetry"
- "github.com/gnolang/gno/tm2/pkg/telemetry/metrics"
- "go.opentelemetry.io/otel/attribute"
- "go.opentelemetry.io/otel/metric"
)
type vmHandler struct {
@@ -107,34 +102,9 @@ func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) abci.ResponseQ
secondPart(req.Path), req.Path)))
}
- // Log the telemetry
- logQueryTelemetry(path, res.IsErr())
-
return res
}
-// logQueryTelemetry logs the relevant VM query telemetry
-func logQueryTelemetry(path string, isErr bool) {
- if !telemetry.MetricsEnabled() {
- return
- }
-
- metrics.VMQueryCalls.Add(
- context.Background(),
- 1,
- metric.WithAttributes(
- attribute.KeyValue{
- Key: "path",
- Value: attribute.StringValue(path),
- },
- ),
- )
-
- if isErr {
- metrics.VMQueryErrors.Add(context.Background(), 1)
- }
-}
-
// queryPackage fetch a package's files.
func (vh vmHandler) queryPackage(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) {
res.Data = []byte(fmt.Sprintf("TODO: parse parts get or make fileset..."))
diff --git a/gno.land/pkg/sdk/vm/handler_test.go b/gno.land/pkg/sdk/vm/handler_test.go
index 38ac8fa61b9..0d238deed1f 100644
--- a/gno.land/pkg/sdk/vm/handler_test.go
+++ b/gno.land/pkg/sdk/vm/handler_test.go
@@ -1,8 +1,13 @@
package vm
import (
+ "fmt"
"testing"
+ "github.com/gnolang/gno/gnovm"
+ abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
+ "github.com/gnolang/gno/tm2/pkg/crypto"
+ "github.com/gnolang/gno/tm2/pkg/std"
"github.com/stretchr/testify/assert"
)
@@ -48,3 +53,272 @@ func Test_parseQueryEval_panic(t *testing.T) {
parseQueryEvalData("gno.land/r/demo/users")
})
}
+
+func TestVmHandlerQuery_Eval(t *testing.T) {
+ tt := []struct {
+ input []byte
+ expectedResult string
+ expectedResultMatch string
+ expectedErrorMatch string
+ expectedPanicMatch string
+ // XXX: expectedEvents
+ }{
+ // valid queries
+ {input: []byte(`gno.land/r/hello.Echo("hello")`), expectedResult: `("echo:hello" string)`},
+ {input: []byte(`gno.land/r/hello.caller()`), expectedResult: `("" std.Address)`}, // FIXME?
+ {input: []byte(`gno.land/r/hello.GetHeight()`), expectedResult: `(0 int64)`},
+ // {input: []byte(`gno.land/r/hello.time.RFC3339`), expectedResult: `test`}, // not working, but should we care?
+ {input: []byte(`gno.land/r/hello.PubString`), expectedResult: `("public string" string)`},
+ {input: []byte(`gno.land/r/hello.ConstString`), expectedResult: `("const string" string)`},
+ {input: []byte(`gno.land/r/hello.pvString`), expectedResult: `("private string" string)`},
+ {input: []byte(`gno.land/r/hello.counter`), expectedResult: `(42 int)`},
+ {input: []byte(`gno.land/r/hello.GetCounter()`), expectedResult: `(42 int)`},
+ {input: []byte(`gno.land/r/hello.Inc()`), expectedResult: `(43 int)`},
+ {input: []byte(`gno.land/r/hello.pvEcho("hello")`), expectedResult: `("pvecho:hello" string)`},
+ {input: []byte(`gno.land/r/hello.1337`), expectedResult: `(1337 int)`},
+ {input: []byte(`gno.land/r/hello.13.37`), expectedResult: `(13.37 float64)`},
+ {input: []byte(`gno.land/r/hello.float64(1337)`), expectedResult: `(1337 float64)`},
+ {input: []byte(`gno.land/r/hello.myStructInst`), expectedResult: `(struct{(1000 int)} gno.land/r/hello.myStruct)`},
+ {input: []byte(`gno.land/r/hello.myStructInst.Foo()`), expectedResult: `("myStruct.Foo" string)`},
+ {input: []byte(`gno.land/r/hello.myStruct`), expectedResultMatch: `\(typeval{gno.land/r/hello.myStruct \(0x.*\)} type{}\)`},
+ {input: []byte(`gno.land/r/hello.Inc`), expectedResult: `(Inc func()( int))`},
+ {input: []byte(`gno.land/r/hello.fn()("hi")`), expectedResult: `("echo:hi" string)`},
+ {input: []byte(`gno.land/r/hello.sl`), expectedResultMatch: `(slice[ref(.*)] []int)`}, // XXX: should return the actual value
+ {input: []byte(`gno.land/r/hello.sl[1]`), expectedResultMatch: `(slice[ref(.*)] []int)`}, // XXX: should return the actual value
+ {input: []byte(`gno.land/r/hello.println(1234)`), expectedResultMatch: `^$`}, // XXX: compare stdout?
+
+ // panics
+ {input: []byte(`gno.land/r/hello`), expectedPanicMatch: `expected . syntax in query input data`},
+
+ // errors
+ {input: []byte(`gno.land/r/hello.doesnotexist`), expectedErrorMatch: `^/:0:0: name doesnotexist not declared:`}, // multiline error
+ {input: []byte(`gno.land/r/doesnotexist.Foo`), expectedErrorMatch: `^invalid package path$`},
+ {input: []byte(`gno.land/r/hello.Panic()`), expectedErrorMatch: `^foo$`},
+ {input: []byte(`gno.land/r/hello.sl[6]`), expectedErrorMatch: `^slice index out of bounds: 6 \(len=5\)$`},
+ }
+
+ for _, tc := range tt {
+ name := string(tc.input)
+ t.Run(name, func(t *testing.T) {
+ env := setupTestEnv()
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
+ vmHandler := env.vmh
+
+ // Give "addr1" some gnots.
+ addr := crypto.AddressFromPreimage([]byte("addr1"))
+ acc := env.acck.NewAccountWithAddress(ctx, addr)
+ env.acck.SetAccount(ctx, acc)
+ env.bank.SetCoins(ctx, addr, std.MustParseCoins("10000000ugnot"))
+ assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins("10000000ugnot")))
+
+ // Create test package.
+ files := []*gnovm.MemFile{
+ {Name: "hello.gno", Body: `
+package hello
+
+import "std"
+import "time"
+
+var _ = time.RFC3339
+func caller() std.Address { return std.GetOrigCaller() }
+var GetHeight = std.GetHeight
+var sl = []int{1,2,3,4,5}
+func fn() func(string) string { return Echo }
+type myStruct struct{a int}
+var myStructInst = myStruct{a: 1000}
+func (ms myStruct) Foo() string { return "myStruct.Foo" }
+func Panic() { panic("foo") }
+var counter int = 42
+var pvString = "private string"
+var PubString = "public string"
+const ConstString = "const string"
+func Echo(msg string) string { return "echo:"+msg }
+func GetCounter() int { return counter }
+func Inc() int { counter += 1; return counter }
+func pvEcho(msg string) string { return "pvecho:"+msg }
+`},
+ }
+ pkgPath := "gno.land/r/hello"
+ msg1 := NewMsgAddPackage(addr, pkgPath, files)
+ err := env.vmk.AddPackage(ctx, msg1)
+ assert.NoError(t, err)
+ env.vmk.CommitGnoTransactionStore(ctx)
+
+ req := abci.RequestQuery{
+ Path: "vm/qeval",
+ Data: tc.input,
+ }
+
+ defer func() {
+ if r := recover(); r != nil {
+ output := fmt.Sprintf("%v", r)
+ assert.Regexp(t, tc.expectedPanicMatch, output)
+ } else {
+ assert.Equal(t, tc.expectedPanicMatch, "", "should not panic")
+ }
+ }()
+ res := vmHandler.Query(env.ctx, req)
+ if tc.expectedPanicMatch == "" {
+ if tc.expectedErrorMatch == "" {
+ assert.True(t, res.IsOK(), "should not have error")
+ if tc.expectedResult != "" {
+ assert.Equal(t, string(res.Data), tc.expectedResult)
+ }
+ if tc.expectedResultMatch != "" {
+ assert.Regexp(t, tc.expectedResultMatch, string(res.Data))
+ }
+ } else {
+ assert.False(t, res.IsOK(), "should have an error")
+ errmsg := res.Error.Error()
+ assert.Regexp(t, tc.expectedErrorMatch, errmsg)
+ }
+ }
+ })
+ }
+}
+
+func TestVmHandlerQuery_Funcs(t *testing.T) {
+ tt := []struct {
+ input []byte
+ expectedResult string
+ expectedErrorMatch string
+ }{
+ // valid queries
+ {input: []byte(`gno.land/r/hello`), expectedResult: `[{"FuncName":"Panic","Params":null,"Results":null},{"FuncName":"Echo","Params":[{"Name":"msg","Type":"string","Value":""}],"Results":[{"Name":"_","Type":"string","Value":""}]},{"FuncName":"GetCounter","Params":null,"Results":[{"Name":"_","Type":"int","Value":""}]},{"FuncName":"Inc","Params":null,"Results":[{"Name":"_","Type":"int","Value":""}]}]`},
+ {input: []byte(`gno.land/r/doesnotexist`), expectedErrorMatch: `invalid package path`},
+ {input: []byte(`std`), expectedErrorMatch: `invalid package path`},
+ {input: []byte(`strings`), expectedErrorMatch: `invalid package path`},
+ }
+
+ for _, tc := range tt {
+ name := string(tc.input)
+ t.Run(name, func(t *testing.T) {
+ env := setupTestEnv()
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
+ vmHandler := env.vmh
+
+ // Give "addr1" some gnots.
+ addr := crypto.AddressFromPreimage([]byte("addr1"))
+ acc := env.acck.NewAccountWithAddress(ctx, addr)
+ env.acck.SetAccount(ctx, acc)
+ env.bank.SetCoins(ctx, addr, std.MustParseCoins("10000000ugnot"))
+ assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins("10000000ugnot")))
+
+ // Create test package.
+ files := []*gnovm.MemFile{
+ {Name: "hello.gno", Body: `
+package hello
+
+var sl = []int{1,2,3,4,5}
+func fn() func(string) string { return Echo }
+type myStruct struct{a int}
+var myStructInst = myStruct{a: 1000}
+func (ms myStruct) Foo() string { return "myStruct.Foo" }
+func Panic() { panic("foo") }
+var counter int = 42
+var pvString = "private string"
+var PubString = "public string"
+const ConstString = "const string"
+func Echo(msg string) string { return "echo:"+msg }
+func GetCounter() int { return counter }
+func Inc() int { counter += 1; return counter }
+func pvEcho(msg string) string { return "pvecho:"+msg }
+`},
+ }
+ pkgPath := "gno.land/r/hello"
+ msg1 := NewMsgAddPackage(addr, pkgPath, files)
+ err := env.vmk.AddPackage(ctx, msg1)
+ assert.NoError(t, err)
+
+ req := abci.RequestQuery{
+ Path: "vm/qfuncs",
+ Data: tc.input,
+ }
+
+ res := vmHandler.Query(env.ctx, req)
+ if tc.expectedErrorMatch == "" {
+ assert.True(t, res.IsOK(), "should not have error")
+ if tc.expectedResult != "" {
+ assert.Equal(t, string(res.Data), tc.expectedResult)
+ }
+ } else {
+ assert.False(t, res.IsOK(), "should have an error")
+ errmsg := res.Error.Error()
+ assert.Regexp(t, tc.expectedErrorMatch, errmsg)
+ }
+ })
+ }
+}
+
+func TestVmHandlerQuery_File(t *testing.T) {
+ tt := []struct {
+ input []byte
+ expectedResult string
+ expectedResultMatch string
+ expectedErrorMatch string
+ expectedPanicMatch string
+ // XXX: expectedEvents
+ }{
+ // valid queries
+ {input: []byte(`gno.land/r/hello/hello.gno`), expectedResult: "package hello\n\nfunc Hello() string { return \"hello\" }\n"},
+ {input: []byte(`gno.land/r/hello/README.md`), expectedResult: "# Hello"},
+ {input: []byte(`gno.land/r/hello/doesnotexist.gno`), expectedErrorMatch: `file "gno.land/r/hello/doesnotexist.gno" is not available`},
+ {input: []byte(`gno.land/r/hello`), expectedResult: "README.md\nhello.gno"},
+ {input: []byte(`gno.land/r/doesnotexist`), expectedErrorMatch: `package "gno.land/r/doesnotexist" is not available`},
+ {input: []byte(`gno.land/r/doesnotexist/hello.gno`), expectedErrorMatch: `file "gno.land/r/doesnotexist/hello.gno" is not available`},
+ }
+
+ for _, tc := range tt {
+ name := string(tc.input)
+ t.Run(name, func(t *testing.T) {
+ env := setupTestEnv()
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
+ vmHandler := env.vmh
+
+ // Give "addr1" some gnots.
+ addr := crypto.AddressFromPreimage([]byte("addr1"))
+ acc := env.acck.NewAccountWithAddress(ctx, addr)
+ env.acck.SetAccount(ctx, acc)
+ env.bank.SetCoins(ctx, addr, std.MustParseCoins("10000000ugnot"))
+ assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins("10000000ugnot")))
+
+ // Create test package.
+ files := []*gnovm.MemFile{
+ {Name: "README.md", Body: "# Hello"},
+ {Name: "hello.gno", Body: "package hello\n\nfunc Hello() string { return \"hello\" }\n"},
+ }
+ pkgPath := "gno.land/r/hello"
+ msg1 := NewMsgAddPackage(addr, pkgPath, files)
+ err := env.vmk.AddPackage(ctx, msg1)
+ assert.NoError(t, err)
+
+ req := abci.RequestQuery{
+ Path: "vm/qfile",
+ Data: tc.input,
+ }
+
+ defer func() {
+ if r := recover(); r != nil {
+ output := fmt.Sprintf("%v", r)
+ assert.Regexp(t, tc.expectedPanicMatch, output)
+ } else {
+ assert.Equal(t, "", tc.expectedPanicMatch, "should not panic")
+ }
+ }()
+ res := vmHandler.Query(env.ctx, req)
+ if tc.expectedErrorMatch == "" {
+ assert.True(t, res.IsOK(), "should not have error")
+ if tc.expectedResult != "" {
+ assert.Equal(t, string(res.Data), tc.expectedResult)
+ }
+ if tc.expectedResultMatch != "" {
+ assert.Regexp(t, tc.expectedResultMatch, string(res.Data))
+ }
+ } else {
+ assert.False(t, res.IsOK(), "should have an error")
+ errmsg := res.Error.Error()
+ assert.Regexp(t, tc.expectedErrorMatch, errmsg)
+ }
+ })
+ }
+}
diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go
index 615be2029fe..bf16cd44243 100644
--- a/gno.land/pkg/sdk/vm/keeper.go
+++ b/gno.land/pkg/sdk/vm/keeper.go
@@ -5,15 +5,17 @@ package vm
import (
"bytes"
"context"
+ goerrors "errors"
"fmt"
+ "io"
"log/slog"
- "os"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
+ "github.com/gnolang/gno/gnovm"
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
"github.com/gnolang/gno/gnovm/stdlibs"
"github.com/gnolang/gno/tm2/pkg/crypto"
@@ -23,6 +25,7 @@ import (
"github.com/gnolang/gno/tm2/pkg/sdk"
"github.com/gnolang/gno/tm2/pkg/sdk/auth"
"github.com/gnolang/gno/tm2/pkg/sdk/bank"
+ "github.com/gnolang/gno/tm2/pkg/sdk/params"
"github.com/gnolang/gno/tm2/pkg/std"
"github.com/gnolang/gno/tm2/pkg/store"
"github.com/gnolang/gno/tm2/pkg/store/dbadapter"
@@ -34,8 +37,8 @@ import (
)
const (
- maxAllocTx = 500 * 1000 * 1000
- maxAllocQuery = 1500 * 1000 * 1000 // higher limit for queries
+ maxAllocTx = 500_000_000
+ maxAllocQuery = 1_500_000_000 // higher limit for queries
)
// vm.VMKeeperI defines a module interface that supports Gno
@@ -45,22 +48,27 @@ type VMKeeperI interface {
Call(ctx sdk.Context, msg MsgCall) (res string, err error)
QueryEval(ctx sdk.Context, pkgPath string, expr string) (res string, err error)
Run(ctx sdk.Context, msg MsgRun) (res string, err error)
+ LoadStdlib(ctx sdk.Context, stdlibDir string)
+ LoadStdlibCached(ctx sdk.Context, stdlibDir string)
+ MakeGnoTransactionStore(ctx sdk.Context) sdk.Context
+ CommitGnoTransactionStore(ctx sdk.Context)
}
var _ VMKeeperI = &VMKeeper{}
// VMKeeper holds all package code and store state.
type VMKeeper struct {
- baseKey store.StoreKey
- iavlKey store.StoreKey
- acck auth.AccountKeeper
- bank bank.BankKeeper
- stdlibsDir string
+ // Needs to be explicitly set, like in the case of gnodev.
+ Output io.Writer
+
+ baseKey store.StoreKey
+ iavlKey store.StoreKey
+ acck auth.AccountKeeper
+ bank bank.BankKeeper
+ prmk params.ParamsKeeper
// cached, the DeliverTx persistent state.
gnoStore gno.Store
-
- maxCycles int64 // max allowed cylces on VM executions
}
// NewVMKeeper returns a new VMKeeper.
@@ -69,87 +77,44 @@ func NewVMKeeper(
iavlKey store.StoreKey,
acck auth.AccountKeeper,
bank bank.BankKeeper,
- stdlibsDir string,
- maxCycles int64,
+ prmk params.ParamsKeeper,
) *VMKeeper {
- // TODO: create an Options struct to avoid too many constructor parameters
vmk := &VMKeeper{
- baseKey: baseKey,
- iavlKey: iavlKey,
- acck: acck,
- bank: bank,
- stdlibsDir: stdlibsDir,
- maxCycles: maxCycles,
+ baseKey: baseKey,
+ iavlKey: iavlKey,
+ acck: acck,
+ bank: bank,
+ prmk: prmk,
}
+
return vmk
}
func (vm *VMKeeper) Initialize(
logger *slog.Logger,
ms store.MultiStore,
- cacheStdlibLoad bool,
) {
if vm.gnoStore != nil {
panic("should not happen")
}
- baseSDKStore := ms.GetStore(vm.baseKey)
- iavlSDKStore := ms.GetStore(vm.iavlKey)
-
- if cacheStdlibLoad {
- // Testing case (using the cache speeds up starting many nodes)
- vm.gnoStore = cachedStdlibLoad(vm.stdlibsDir, baseSDKStore, iavlSDKStore)
- } else {
- // On-chain case
- vm.gnoStore = uncachedPackageLoad(logger, vm.stdlibsDir, baseSDKStore, iavlSDKStore)
- }
-}
+ baseStore := ms.GetStore(vm.baseKey)
+ iavlStore := ms.GetStore(vm.iavlKey)
-func uncachedPackageLoad(
- logger *slog.Logger,
- stdlibsDir string,
- baseStore, iavlStore store.Store,
-) gno.Store {
alloc := gno.NewAllocator(maxAllocTx)
- gnoStore := gno.NewStore(alloc, baseStore, iavlStore)
- gnoStore.SetNativeStore(stdlibs.NativeStore)
- if gnoStore.NumMemPackages() == 0 {
- // No packages in the store; set up the stdlibs.
- start := time.Now()
-
- loadStdlib(stdlibsDir, gnoStore)
-
- // XXX Quick and dirty to make this function work on non-validator nodes
- iter := iavlStore.Iterator(nil, nil)
- for ; iter.Valid(); iter.Next() {
- baseStore.Set(append(iavlBackupPrefix, iter.Key()...), iter.Value())
- }
- iter.Close()
+ vm.gnoStore = gno.NewStore(alloc, baseStore, iavlStore)
+ vm.gnoStore.SetNativeResolver(stdlibs.NativeResolver)
- logger.Debug("Standard libraries initialized",
- "elapsed", time.Since(start))
- } else {
+ if vm.gnoStore.NumMemPackages() > 0 {
// for now, all mem packages must be re-run after reboot.
// TODO remove this, and generally solve for in-mem garbage collection
// and memory management across many objects/types/nodes/packages.
start := time.Now()
- // XXX Quick and dirty to make this function work on non-validator nodes
- if isStoreEmpty(iavlStore) {
- iter := baseStore.Iterator(iavlBackupPrefix, nil)
- for ; iter.Valid(); iter.Next() {
- if !bytes.HasPrefix(iter.Key(), iavlBackupPrefix) {
- break
- }
- iavlStore.Set(iter.Key()[len(iavlBackupPrefix):], iter.Value())
- }
- iter.Close()
- }
-
m2 := gno.NewMachineWithOptions(
gno.MachineOptions{
PkgPath: "",
- Output: os.Stdout, // XXX
- Store: gnoStore,
+ Output: vm.Output,
+ Store: vm.gnoStore,
})
defer m2.Release()
gno.DisableDebug()
@@ -159,57 +124,52 @@ func uncachedPackageLoad(
logger.Debug("GnoVM packages preprocessed",
"elapsed", time.Since(start))
}
- return gnoStore
}
-var iavlBackupPrefix = []byte("init_iavl_backup:")
-
-func isStoreEmpty(st store.Store) bool {
- iter := st.Iterator(nil, nil)
- defer iter.Close()
- for ; iter.Valid(); iter.Next() {
- return false
- }
- return true
+type stdlibCache struct {
+ dir string
+ base store.Store
+ iavl store.Store
+ gno gno.Store
}
-func cachedStdlibLoad(stdlibsDir string, baseStore, iavlStore store.Store) gno.Store {
+var (
+ cachedStdlibOnce sync.Once
+ cachedStdlib stdlibCache
+)
+
+// LoadStdlib loads the Gno standard library into the given store.
+func (vm *VMKeeper) LoadStdlibCached(ctx sdk.Context, stdlibDir string) {
cachedStdlibOnce.Do(func() {
- cachedStdlibBase = memdb.NewMemDB()
- cachedStdlibIavl = memdb.NewMemDB()
-
- cachedGnoStore = gno.NewStore(nil,
- dbadapter.StoreConstructor(cachedStdlibBase, types.StoreOptions{}),
- dbadapter.StoreConstructor(cachedStdlibIavl, types.StoreOptions{}))
- cachedGnoStore.SetNativeStore(stdlibs.NativeStore)
- loadStdlib(stdlibsDir, cachedGnoStore)
- })
+ cachedStdlib = stdlibCache{
+ dir: stdlibDir,
+ base: dbadapter.StoreConstructor(memdb.NewMemDB(), types.StoreOptions{}),
+ iavl: dbadapter.StoreConstructor(memdb.NewMemDB(), types.StoreOptions{}),
+ }
- itr := cachedStdlibBase.Iterator(nil, nil)
- for ; itr.Valid(); itr.Next() {
- baseStore.Set(itr.Key(), itr.Value())
- }
+ gs := gno.NewStore(nil, cachedStdlib.base, cachedStdlib.iavl)
+ gs.SetNativeResolver(stdlibs.NativeResolver)
+ loadStdlib(gs, stdlibDir)
+ cachedStdlib.gno = gs
+ })
- itr = cachedStdlibIavl.Iterator(nil, nil)
- for ; itr.Valid(); itr.Next() {
- iavlStore.Set(itr.Key(), itr.Value())
+ if stdlibDir != cachedStdlib.dir {
+ panic(fmt.Sprintf(
+ "cannot load cached stdlib: cached stdlib is in dir %q; wanted to load stdlib in dir %q",
+ cachedStdlib.dir, stdlibDir))
}
- alloc := gno.NewAllocator(maxAllocTx)
- gs := gno.NewStore(alloc, baseStore, iavlStore)
- gs.SetNativeStore(stdlibs.NativeStore)
- gno.CopyCachesFromStore(gs, cachedGnoStore)
- return gs
+ gs := vm.getGnoTransactionStore(ctx)
+ gno.CopyFromCachedStore(gs, cachedStdlib.gno, cachedStdlib.base, cachedStdlib.iavl)
}
-var (
- cachedStdlibOnce sync.Once
- cachedStdlibBase *memdb.MemDB
- cachedStdlibIavl *memdb.MemDB
- cachedGnoStore gno.Store
-)
+// LoadStdlib loads the Gno standard library into the given store.
+func (vm *VMKeeper) LoadStdlib(ctx sdk.Context, stdlibDir string) {
+ gs := vm.getGnoTransactionStore(ctx)
+ loadStdlib(gs, stdlibDir)
+}
-func loadStdlib(stdlibsDir string, store gno.Store) {
+func loadStdlib(store gno.Store, stdlibDir string) {
stdlibInitList := stdlibs.InitOrder()
for _, lib := range stdlibInitList {
if lib == "testing" {
@@ -217,76 +177,74 @@ func loadStdlib(stdlibsDir string, store gno.Store) {
// like fmt and encoding/json
continue
}
- loadStdlibPackage(lib, stdlibsDir, store)
+ loadStdlibPackage(lib, stdlibDir, store)
}
}
-func loadStdlibPackage(pkgPath, stdlibsDir string, store gno.Store) {
- stdlibPath := filepath.Join(stdlibsDir, pkgPath)
+func loadStdlibPackage(pkgPath, stdlibDir string, store gno.Store) {
+ stdlibPath := filepath.Join(stdlibDir, pkgPath)
if !osm.DirExists(stdlibPath) {
// does not exist.
panic(fmt.Sprintf("failed loading stdlib %q: does not exist", pkgPath))
}
- memPkg := gno.ReadMemPackage(stdlibPath, pkgPath)
+ memPkg := gno.MustReadMemPackage(stdlibPath, pkgPath)
if memPkg.IsEmpty() {
// no gno files are present
panic(fmt.Sprintf("failed loading stdlib %q: not a valid MemPackage", pkgPath))
}
m := gno.NewMachineWithOptions(gno.MachineOptions{
+ // XXX: gno.land, vm.domain, other?
PkgPath: "gno.land/r/stdlibs/" + pkgPath,
// PkgPath: pkgPath, XXX why?
- Output: os.Stdout,
- Store: store,
+ Store: store,
})
defer m.Release()
m.RunMemPackage(memPkg, true)
}
-func (vm *VMKeeper) getGnoStore(ctx sdk.Context) gno.Store {
- // construct main store if nil.
- if vm.gnoStore == nil {
- panic("VMKeeper must first be initialized")
- }
- switch ctx.Mode() {
- case sdk.RunTxModeDeliver:
- // swap sdk store of existing store.
- // this is needed due to e.g. gas wrappers.
- baseSDKStore := ctx.Store(vm.baseKey)
- iavlSDKStore := ctx.Store(vm.iavlKey)
- vm.gnoStore.SwapStores(baseSDKStore, iavlSDKStore)
- // clear object cache for every transaction.
- // NOTE: this is inefficient, but simple.
- // in the future, replace with more advanced caching strategy.
- vm.gnoStore.ClearObjectCache()
- return vm.gnoStore
- case sdk.RunTxModeCheck:
- // For query??? XXX Why not RunTxModeQuery?
- simStore := vm.gnoStore.Fork()
- baseSDKStore := ctx.Store(vm.baseKey)
- iavlSDKStore := ctx.Store(vm.iavlKey)
- simStore.SwapStores(baseSDKStore, iavlSDKStore)
- return simStore
- case sdk.RunTxModeSimulate:
- // always make a new store for simulate for isolation.
- simStore := vm.gnoStore.Fork()
- baseSDKStore := ctx.Store(vm.baseKey)
- iavlSDKStore := ctx.Store(vm.iavlKey)
- simStore.SwapStores(baseSDKStore, iavlSDKStore)
- return simStore
- default:
- panic("should not happen")
- }
+type gnoStoreContextKeyType struct{}
+
+var gnoStoreContextKey gnoStoreContextKeyType
+
+func (vm *VMKeeper) newGnoTransactionStore(ctx sdk.Context) gno.TransactionStore {
+ base := ctx.Store(vm.baseKey)
+ iavl := ctx.Store(vm.iavlKey)
+ gasMeter := ctx.GasMeter()
+
+ return vm.gnoStore.BeginTransaction(base, iavl, gasMeter)
+}
+
+func (vm *VMKeeper) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context {
+ return ctx.WithValue(gnoStoreContextKey, vm.newGnoTransactionStore(ctx))
+}
+
+func (vm *VMKeeper) CommitGnoTransactionStore(ctx sdk.Context) {
+ vm.getGnoTransactionStore(ctx).Write()
+}
+
+func (vm *VMKeeper) getGnoTransactionStore(ctx sdk.Context) gno.TransactionStore {
+ txStore := ctx.Value(gnoStoreContextKey).(gno.TransactionStore)
+ txStore.ClearObjectCache()
+ return txStore
}
// Namespace can be either a user or crypto address.
-var reNamespace = regexp.MustCompile(`^gno.land/(?:r|p)/([\.~_a-zA-Z0-9]+)`)
+var reNamespace = regexp.MustCompile(`^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/(?: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"
+ sysUsersPkg := vm.getSysUsersPkgParam(ctx)
+ if sysUsersPkg == "" {
+ return nil
+ }
+ chainDomain := vm.getChainDomainParam(ctx)
+
+ store := vm.getGnoTransactionStore(ctx)
- store := vm.getGnoStore(ctx)
+ if !strings.HasPrefix(pkgPath, chainDomain+"/") {
+ return ErrInvalidPkgPath(pkgPath) // no match
+ }
match := reNamespace.FindStringSubmatch(pkgPath)
switch len(match) {
@@ -296,9 +254,6 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add
default:
panic("invalid pattern while matching pkgpath")
}
- if len(match) != 2 {
- return ErrInvalidPkgPath(pkgPath)
- }
username := match[1]
// if `sysUsersPkg` does not exist -> skip validation.
@@ -311,6 +266,7 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add
pkgAddr := gno.DerivePkgAddr(pkgPath)
msgCtx := stdlibs.ExecContext{
ChainID: ctx.ChainID(),
+ ChainDomain: chainDomain,
Height: ctx.BlockHeight(),
Timestamp: ctx.BlockTime().Unix(),
OrigCaller: creator.Bech32(),
@@ -318,18 +274,18 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add
OrigPkgAddr: pkgAddr.Bech32(),
// XXX: should we remove the banker ?
Banker: NewSDKBanker(vm, ctx),
+ Params: NewSDKParams(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(),
+ PkgPath: "",
+ Output: vm.Output,
+ Store: store,
+ Context: msgCtx,
+ Alloc: store.GetAllocator(),
+ GasMeter: ctx.GasMeter(),
})
defer m.Release()
@@ -367,7 +323,8 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) {
pkgPath := msg.Package.Path
memPkg := msg.Package
deposit := msg.Deposit
- gnostore := vm.getGnoStore(ctx)
+ gnostore := vm.getGnoTransactionStore(ctx)
+ chainDomain := vm.getChainDomainParam(ctx)
// Validate arguments.
if creator.IsZero() {
@@ -380,8 +337,11 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) {
if err := msg.Package.Validate(); err != nil {
return ErrInvalidPkgPath(err.Error())
}
+ if !strings.HasPrefix(pkgPath, chainDomain+"/") {
+ return ErrInvalidPkgPath("invalid domain: " + pkgPath)
+ }
if pv := gnostore.GetPackage(pkgPath, false); pv != nil {
- return ErrInvalidPkgPath("package already exists: " + pkgPath)
+ return ErrPkgAlreadyExists("package already exists: " + pkgPath)
}
if gno.ReGnoRunPath.MatchString(pkgPath) {
return ErrInvalidPkgPath("reserved package name: " + pkgPath)
@@ -411,40 +371,29 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) {
// Parse and run the files, construct *PV.
msgCtx := stdlibs.ExecContext{
ChainID: ctx.ChainID(),
+ ChainDomain: chainDomain,
Height: ctx.BlockHeight(),
Timestamp: ctx.BlockTime().Unix(),
- Msg: msg,
OrigCaller: creator.Bech32(),
OrigSend: deposit,
OrigSendSpent: new(std.Coins),
OrigPkgAddr: pkgAddr.Bech32(),
Banker: NewSDKBanker(vm, ctx),
+ Params: NewSDKParams(vm, ctx),
EventLogger: ctx.EventLogger(),
}
// Parse and run the files, construct *PV.
m2 := gno.NewMachineWithOptions(
gno.MachineOptions{
- PkgPath: "",
- Output: os.Stdout, // XXX
- Store: gnostore,
- Alloc: gnostore.GetAllocator(),
- Context: msgCtx,
- MaxCycles: vm.maxCycles,
- GasMeter: ctx.GasMeter(),
+ PkgPath: "",
+ Output: vm.Output,
+ Store: gnostore,
+ Alloc: gnostore.GetAllocator(),
+ Context: msgCtx,
+ GasMeter: ctx.GasMeter(),
})
defer m2.Release()
- defer func() {
- if r := recover(); r != nil {
- switch r.(type) {
- case store.OutOfGasException: // panic in consumeGas()
- panic(r)
- default:
- err = errors.Wrap(fmt.Errorf("%v", r), "VM addpkg panic: %v\n%s\n",
- r, m2.String())
- return
- }
- }
- }()
+ defer doRecover(m2, &err)
m2.RunMemPackage(memPkg, true)
// Log the telemetry
@@ -464,7 +413,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) {
func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) {
pkgPath := msg.PkgPath // to import
fnc := msg.Func
- gnostore := vm.getGnoStore(ctx)
+ gnostore := vm.getGnoTransactionStore(ctx)
// Get the package and function type.
pv := gnostore.GetPackage(pkgPath, false)
pl := gno.PackageNodeLocation(pkgPath)
@@ -510,46 +459,33 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) {
// Make context.
// NOTE: if this is too expensive,
// could it be safely partially memoized?
+ chainDomain := vm.getChainDomainParam(ctx)
msgCtx := stdlibs.ExecContext{
ChainID: ctx.ChainID(),
+ ChainDomain: chainDomain,
Height: ctx.BlockHeight(),
Timestamp: ctx.BlockTime().Unix(),
- Msg: msg,
OrigCaller: caller.Bech32(),
OrigSend: send,
OrigSendSpent: new(std.Coins),
OrigPkgAddr: pkgAddr.Bech32(),
Banker: NewSDKBanker(vm, ctx),
+ Params: NewSDKParams(vm, ctx),
EventLogger: ctx.EventLogger(),
}
// Construct machine and evaluate.
m := gno.NewMachineWithOptions(
gno.MachineOptions{
- PkgPath: "",
- Output: os.Stdout, // XXX
- Store: gnostore,
- Context: msgCtx,
- Alloc: gnostore.GetAllocator(),
- MaxCycles: vm.maxCycles,
- GasMeter: ctx.GasMeter(),
+ PkgPath: "",
+ Output: vm.Output,
+ Store: gnostore,
+ Context: msgCtx,
+ Alloc: gnostore.GetAllocator(),
+ GasMeter: ctx.GasMeter(),
})
defer m.Release()
m.SetActivePackage(mpv)
- defer func() {
- if r := recover(); r != nil {
- switch r := r.(type) {
- case store.OutOfGasException: // panic in consumeGas()
- panic(r)
- case gno.UnhandledPanicError:
- err = errors.Wrap(fmt.Errorf("%v", r.Error()), "VM call panic: %s\nStacktrace: %s\n",
- r.Error(), m.ExceptionsStacktrace())
- default:
- err = errors.Wrap(fmt.Errorf("%v", r), "VM call panic: %v\nMachine State:%s\nStacktrace: %s\n",
- r, m.String(), m.Stacktrace().String())
- return
- }
- }
- }()
+ defer doRecover(m, &err)
rtvs := m.Eval(xn)
for i, rtv := range rtvs {
res = res + rtv.String()
@@ -574,18 +510,48 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) {
// TODO pay for gas? TODO see context?
}
+func doRecover(m *gno.Machine, e *error) {
+ r := recover()
+ if r == nil {
+ return
+ }
+ if err, ok := r.(error); ok {
+ var oog types.OutOfGasError
+ if goerrors.As(err, &oog) {
+ // Re-panic and don't wrap.
+ panic(oog)
+ }
+ var up gno.UnhandledPanicError
+ if goerrors.As(err, &up) {
+ // Common unhandled panic error, skip machine state.
+ *e = errors.Wrapf(
+ errors.New(up.Descriptor),
+ "VM panic: %s\nStacktrace: %s\n",
+ up.Descriptor, m.ExceptionsStacktrace(),
+ )
+ return
+ }
+ }
+ *e = errors.Wrapf(
+ fmt.Errorf("%v", r),
+ "VM panic: %v\nMachine State:%s\nStacktrace: %s\n",
+ r, m.String(), m.Stacktrace().String(),
+ )
+}
+
// Run executes arbitrary Gno code in the context of the caller's realm.
func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) {
caller := msg.Caller
pkgAddr := caller
- gnostore := vm.getGnoStore(ctx)
+ gnostore := vm.getGnoTransactionStore(ctx)
send := msg.Send
memPkg := msg.Package
+ chainDomain := vm.getChainDomainParam(ctx)
// coerce path to right one.
// the path in the message must be "" or the following path.
// this is already checked in MsgRun.ValidateBasic
- memPkg.Path = "gno.land/r/" + msg.Caller.String() + "/run"
+ memPkg.Path = chainDomain + "/r/" + msg.Caller.String() + "/run"
// Validate arguments.
callerAcc := vm.acck.GetAccount(ctx, caller)
@@ -611,69 +577,60 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) {
// Parse and run the files, construct *PV.
msgCtx := stdlibs.ExecContext{
ChainID: ctx.ChainID(),
+ ChainDomain: chainDomain,
Height: ctx.BlockHeight(),
Timestamp: ctx.BlockTime().Unix(),
- Msg: msg,
OrigCaller: caller.Bech32(),
OrigSend: send,
OrigSendSpent: new(std.Coins),
OrigPkgAddr: pkgAddr.Bech32(),
Banker: NewSDKBanker(vm, ctx),
+ Params: NewSDKParams(vm, ctx),
EventLogger: ctx.EventLogger(),
}
- // Parse and run the files, construct *PV.
+
buf := new(bytes.Buffer)
- m := gno.NewMachineWithOptions(
- gno.MachineOptions{
- PkgPath: "",
- Output: buf,
- Store: gnostore,
- Alloc: gnostore.GetAllocator(),
- Context: msgCtx,
- MaxCycles: vm.maxCycles,
- GasMeter: ctx.GasMeter(),
- })
- // XXX MsgRun does not have pkgPath. How do we find it on chain?
- defer m.Release()
- defer func() {
- if r := recover(); r != nil {
- switch r.(type) {
- case store.OutOfGasException: // panic in consumeGas()
- panic(r)
- default:
- err = errors.Wrap(fmt.Errorf("%v", r), "VM run main addpkg panic: %v\n%s\n",
- r, m.String())
- return
- }
+ output := io.Writer(buf)
+
+ // Run as self-executing closure to have own function for doRecover / m.Release defers.
+ pv := func() *gno.PackageValue {
+ // Parse and run the files, construct *PV.
+ if vm.Output != nil {
+ output = io.MultiWriter(buf, vm.Output)
}
- }()
+ m := gno.NewMachineWithOptions(
+ gno.MachineOptions{
+ PkgPath: "",
+ Output: output,
+ Store: gnostore,
+ Alloc: gnostore.GetAllocator(),
+ Context: msgCtx,
+ GasMeter: ctx.GasMeter(),
+ })
+ // XXX MsgRun does not have pkgPath. How do we find it on chain?
+ defer m.Release()
+ defer doRecover(m, &err)
- _, pv := m.RunMemPackage(memPkg, false)
+ _, pv := m.RunMemPackage(memPkg, false)
+ return pv
+ }()
+ if err != nil {
+ // handle any errors happened within pv generation.
+ return
+ }
m2 := gno.NewMachineWithOptions(
gno.MachineOptions{
- PkgPath: "",
- Output: buf,
- Store: gnostore,
- Alloc: gnostore.GetAllocator(),
- Context: msgCtx,
- MaxCycles: vm.maxCycles,
- GasMeter: ctx.GasMeter(),
+ PkgPath: "",
+ Output: output,
+ Store: gnostore,
+ Alloc: gnostore.GetAllocator(),
+ Context: msgCtx,
+ GasMeter: ctx.GasMeter(),
})
defer m2.Release()
m2.SetActivePackage(pv)
- defer func() {
- if r := recover(); r != nil {
- switch r.(type) {
- case store.OutOfGasException: // panic in consumeGas()
- panic(r)
- default:
- err = errors.Wrap(fmt.Errorf("%v", r), "VM run main call panic: %v\n%s\n",
- r, m2.String())
- return
- }
- }
- }()
+ defer doRecover(m2, &err)
m2.RunMain()
res = buf.String()
@@ -692,7 +649,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) {
// QueryFuncs returns public facing function signatures.
func (vm *VMKeeper) QueryFuncs(ctx sdk.Context, pkgPath string) (fsigs FunctionSignatures, err error) {
- store := vm.getGnoStore(ctx)
+ store := vm.newGnoTransactionStore(ctx) // throwaway (never committed)
// Ensure pkgPath is realm.
if !gno.IsRealmPath(pkgPath) {
err = ErrInvalidPkgPath(fmt.Sprintf(
@@ -755,7 +712,7 @@ func (vm *VMKeeper) QueryFuncs(ctx sdk.Context, pkgPath string) (fsigs FunctionS
// TODO: then, rename to "Eval".
func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res string, err error) {
alloc := gno.NewAllocator(maxAllocQuery)
- gnostore := vm.getGnoStore(ctx)
+ gnostore := vm.newGnoTransactionStore(ctx) // throwaway (never committed)
pkgAddr := gno.DerivePkgAddr(pkgPath)
// Get Package.
pv := gnostore.GetPackage(pkgPath, false)
@@ -770,41 +727,31 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res
return "", err
}
// Construct new machine.
+ chainDomain := vm.getChainDomainParam(ctx)
msgCtx := stdlibs.ExecContext{
- ChainID: ctx.ChainID(),
- Height: ctx.BlockHeight(),
- Timestamp: ctx.BlockTime().Unix(),
- // Msg: msg,
+ ChainID: ctx.ChainID(),
+ ChainDomain: chainDomain,
+ Height: ctx.BlockHeight(),
+ Timestamp: ctx.BlockTime().Unix(),
// OrigCaller: caller,
// OrigSend: send,
// OrigSendSpent: nil,
OrigPkgAddr: pkgAddr.Bech32(),
Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded.
+ Params: NewSDKParams(vm, ctx),
EventLogger: ctx.EventLogger(),
}
m := gno.NewMachineWithOptions(
gno.MachineOptions{
- PkgPath: pkgPath,
- Output: os.Stdout, // XXX
- Store: gnostore,
- Context: msgCtx,
- Alloc: alloc,
- MaxCycles: vm.maxCycles,
- GasMeter: ctx.GasMeter(),
+ PkgPath: pkgPath,
+ Output: vm.Output,
+ Store: gnostore,
+ Context: msgCtx,
+ Alloc: alloc,
+ GasMeter: ctx.GasMeter(),
})
defer m.Release()
- defer func() {
- if r := recover(); r != nil {
- switch r.(type) {
- case store.OutOfGasException: // panic in consumeGas()
- panic(r)
- default:
- err = errors.Wrap(fmt.Errorf("%v", r), "VM query eval panic: %v\n%s\n",
- r, m.String())
- return
- }
- }
- }()
+ defer doRecover(m, &err)
rtvs := m.Eval(xx)
res = ""
for i, rtv := range rtvs {
@@ -822,7 +769,7 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res
// TODO: then, rename to "EvalString".
func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string) (res string, err error) {
alloc := gno.NewAllocator(maxAllocQuery)
- gnostore := vm.getGnoStore(ctx)
+ gnostore := vm.newGnoTransactionStore(ctx) // throwaway (never committed)
pkgAddr := gno.DerivePkgAddr(pkgPath)
// Get Package.
pv := gnostore.GetPackage(pkgPath, false)
@@ -837,41 +784,31 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string
return "", err
}
// Construct new machine.
+ chainDomain := vm.getChainDomainParam(ctx)
msgCtx := stdlibs.ExecContext{
- ChainID: ctx.ChainID(),
- Height: ctx.BlockHeight(),
- Timestamp: ctx.BlockTime().Unix(),
- // Msg: msg,
+ ChainID: ctx.ChainID(),
+ ChainDomain: chainDomain,
+ Height: ctx.BlockHeight(),
+ Timestamp: ctx.BlockTime().Unix(),
// OrigCaller: caller,
// OrigSend: jsend,
// OrigSendSpent: nil,
OrigPkgAddr: pkgAddr.Bech32(),
Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded.
+ Params: NewSDKParams(vm, ctx),
EventLogger: ctx.EventLogger(),
}
m := gno.NewMachineWithOptions(
gno.MachineOptions{
- PkgPath: pkgPath,
- Output: os.Stdout, // XXX
- Store: gnostore,
- Context: msgCtx,
- Alloc: alloc,
- MaxCycles: vm.maxCycles,
- GasMeter: ctx.GasMeter(),
+ PkgPath: pkgPath,
+ Output: vm.Output,
+ Store: gnostore,
+ Context: msgCtx,
+ Alloc: alloc,
+ GasMeter: ctx.GasMeter(),
})
defer m.Release()
- defer func() {
- if r := recover(); r != nil {
- switch r.(type) {
- case store.OutOfGasException: // panic in consumeGas()
- panic(r)
- default:
- err = errors.Wrap(fmt.Errorf("%v", r), "VM query eval string panic: %v\n%s\n",
- r, m.String())
- return
- }
- }
- }()
+ defer doRecover(m, &err)
rtvs := m.Eval(xx)
if len(rtvs) != 1 {
return "", errors.New("expected 1 string result, got %d", len(rtvs))
@@ -883,8 +820,8 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string
}
func (vm *VMKeeper) QueryFile(ctx sdk.Context, filepath string) (res string, err error) {
- store := vm.getGnoStore(ctx)
- dirpath, filename := std.SplitFilepath(filepath)
+ store := vm.newGnoTransactionStore(ctx) // throwaway (never committed)
+ dirpath, filename := gnovm.SplitFilepath(filepath)
if filename != "" {
memFile := store.GetMemFile(dirpath, filename)
if memFile == nil {
diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go
index 75b55c3174a..f8144988c44 100644
--- a/gno.land/pkg/sdk/vm/keeper_test.go
+++ b/gno.land/pkg/sdk/vm/keeper_test.go
@@ -9,18 +9,24 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
"github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
+ "github.com/gnolang/gno/gnovm"
+ "github.com/gnolang/gno/gnovm/pkg/gnolang"
"github.com/gnolang/gno/tm2/pkg/crypto"
+ "github.com/gnolang/gno/tm2/pkg/db/memdb"
+ "github.com/gnolang/gno/tm2/pkg/log"
"github.com/gnolang/gno/tm2/pkg/std"
+ "github.com/gnolang/gno/tm2/pkg/store/dbadapter"
+ "github.com/gnolang/gno/tm2/pkg/store/types"
)
-var coinsString = ugnot.ValueString(10000000)
+var coinsString = ugnot.ValueString(10_000_000)
func TestVMKeeperAddPackage(t *testing.T) {
env := setupTestEnv()
- ctx := env.ctx
- vmk := env.vmk
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
// Give "addr1" some gnots.
addr := crypto.AddressFromPreimage([]byte("addr1"))
@@ -30,7 +36,7 @@ func TestVMKeeperAddPackage(t *testing.T) {
assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString)))
// Create test package.
- files := []*std.MemFile{
+ files := []*gnovm.MemFile{
{
Name: "test.gno",
Body: `package test
@@ -39,20 +45,20 @@ func Echo() string {return "hello world"}`,
}
pkgPath := "gno.land/r/test"
msg1 := NewMsgAddPackage(addr, pkgPath, files)
- assert.Nil(t, env.vmk.gnoStore.GetPackage(pkgPath, false))
+ assert.Nil(t, env.vmk.getGnoTransactionStore(ctx).GetPackage(pkgPath, false))
err := env.vmk.AddPackage(ctx, msg1)
assert.NoError(t, err)
- assert.NotNil(t, env.vmk.gnoStore.GetPackage(pkgPath, false))
+ assert.NotNil(t, env.vmk.getGnoTransactionStore(ctx).GetPackage(pkgPath, false))
err = env.vmk.AddPackage(ctx, msg1)
assert.Error(t, err)
- assert.True(t, errors.Is(err, InvalidPkgPathError{}))
+ assert.True(t, errors.Is(err, PkgExistError{}))
// added package is formatted
- store := vmk.getGnoStore(ctx)
+ store := env.vmk.getGnoTransactionStore(ctx)
memFile := store.GetMemFile("gno.land/r/test", "test.gno")
assert.NotNil(t, memFile)
expected := `package test
@@ -62,10 +68,47 @@ func Echo() string { return "hello world" }
assert.Equal(t, expected, memFile.Body)
}
+func TestVMKeeperAddPackage_InvalidDomain(t *testing.T) {
+ env := setupTestEnv()
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
+
+ // Give "addr1" some gnots.
+ addr := crypto.AddressFromPreimage([]byte("addr1"))
+ acc := env.acck.NewAccountWithAddress(ctx, addr)
+ env.acck.SetAccount(ctx, acc)
+ env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString))
+ assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString)))
+
+ // Create test package.
+ files := []*gnovm.MemFile{
+ {
+ Name: "test.gno",
+ Body: `package test
+func Echo() string {return "hello world"}`,
+ },
+ }
+ pkgPath := "anotherdomain.land/r/test"
+ msg1 := NewMsgAddPackage(addr, pkgPath, files)
+ assert.Nil(t, env.vmk.getGnoTransactionStore(ctx).GetPackage(pkgPath, false))
+
+ err := env.vmk.AddPackage(ctx, msg1)
+
+ assert.Error(t, err, ErrInvalidPkgPath("invalid domain: anotherdomain.land/r/test"))
+ assert.Nil(t, env.vmk.getGnoTransactionStore(ctx).GetPackage(pkgPath, false))
+
+ err = env.vmk.AddPackage(ctx, msg1)
+ assert.Error(t, err, ErrInvalidPkgPath("invalid domain: anotherdomain.land/r/test"))
+
+ // added package is formatted
+ store := env.vmk.getGnoTransactionStore(ctx)
+ memFile := store.GetMemFile("gno.land/r/test", "test.gno")
+ assert.Nil(t, memFile)
+}
+
// Sending total send amount succeeds.
func TestVMKeeperOrigSend1(t *testing.T) {
env := setupTestEnv()
- ctx := env.ctx
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
// Give "addr1" some gnots.
addr := crypto.AddressFromPreimage([]byte("addr1"))
@@ -75,8 +118,8 @@ func TestVMKeeperOrigSend1(t *testing.T) {
assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString)))
// Create test package.
- files := []*std.MemFile{
- {"init.gno", `
+ files := []*gnovm.MemFile{
+ {Name: "init.gno", Body: `
package test
import "std"
@@ -110,7 +153,7 @@ func Echo(msg string) string {
// Sending too much fails
func TestVMKeeperOrigSend2(t *testing.T) {
env := setupTestEnv()
- ctx := env.ctx
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
// Give "addr1" some gnots.
addr := crypto.AddressFromPreimage([]byte("addr1"))
@@ -120,8 +163,8 @@ func TestVMKeeperOrigSend2(t *testing.T) {
assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString)))
// Create test package.
- files := []*std.MemFile{
- {"init.gno", `
+ files := []*gnovm.MemFile{
+ {Name: "init.gno", Body: `
package test
import "std"
@@ -164,7 +207,7 @@ func GetAdmin() string {
// Sending more than tx send fails.
func TestVMKeeperOrigSend3(t *testing.T) {
env := setupTestEnv()
- ctx := env.ctx
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
// Give "addr1" some gnots.
addr := crypto.AddressFromPreimage([]byte("addr1"))
@@ -174,8 +217,8 @@ func TestVMKeeperOrigSend3(t *testing.T) {
assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString)))
// Create test package.
- files := []*std.MemFile{
- {"init.gno", `
+ files := []*gnovm.MemFile{
+ {Name: "init.gno", Body: `
package test
import "std"
@@ -208,7 +251,7 @@ func Echo(msg string) string {
// Sending realm package coins succeeds.
func TestVMKeeperRealmSend1(t *testing.T) {
env := setupTestEnv()
- ctx := env.ctx
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
// Give "addr1" some gnots.
addr := crypto.AddressFromPreimage([]byte("addr1"))
@@ -218,8 +261,8 @@ func TestVMKeeperRealmSend1(t *testing.T) {
assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString)))
// Create test package.
- files := []*std.MemFile{
- {"init.gno", `
+ files := []*gnovm.MemFile{
+ {Name: "init.gno", Body: `
package test
import "std"
@@ -252,7 +295,7 @@ func Echo(msg string) string {
// Sending too much realm package coins fails.
func TestVMKeeperRealmSend2(t *testing.T) {
env := setupTestEnv()
- ctx := env.ctx
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
// Give "addr1" some gnots.
addr := crypto.AddressFromPreimage([]byte("addr1"))
@@ -262,8 +305,8 @@ func TestVMKeeperRealmSend2(t *testing.T) {
assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString)))
// Create test package.
- files := []*std.MemFile{
- {"init.gno", `
+ files := []*gnovm.MemFile{
+ {Name: "init.gno", Body: `
package test
import "std"
@@ -293,10 +336,64 @@ func Echo(msg string) string {
assert.Error(t, err)
}
+// Using x/params from a realm.
+func TestVMKeeperParams(t *testing.T) {
+ env := setupTestEnv()
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
+
+ // Give "addr1" some gnots.
+ addr := crypto.AddressFromPreimage([]byte("addr1"))
+ acc := env.acck.NewAccountWithAddress(ctx, addr)
+ env.acck.SetAccount(ctx, acc)
+ env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString))
+ // env.prmk.
+ assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString)))
+
+ // Create test package.
+ files := []*gnovm.MemFile{
+ {Name: "init.gno", Body: `
+package test
+
+import "std"
+
+func init() {
+ std.SetParamString("foo.string", "foo1")
+}
+
+func Do() string {
+ std.SetParamInt64("bar.int64", int64(1337))
+ std.SetParamString("foo.string", "foo2") // override init
+
+ return "XXX" // return std.GetConfig("gno.land/r/test.foo"), if we want to expose std.GetConfig, maybe as a std.TestGetConfig
+}`},
+ }
+ pkgPath := "gno.land/r/test"
+ msg1 := NewMsgAddPackage(addr, pkgPath, files)
+ err := env.vmk.AddPackage(ctx, msg1)
+ assert.NoError(t, err)
+
+ // Run Echo function.
+ coins := std.MustParseCoins(ugnot.ValueString(9_000_000))
+ msg2 := NewMsgCall(addr, coins, pkgPath, "Do", []string{})
+
+ res, err := env.vmk.Call(ctx, msg2)
+ assert.NoError(t, err)
+ _ = res
+ expected := fmt.Sprintf("(\"%s\" string)\n\n", "XXX") // XXX: return something more useful
+ assert.Equal(t, expected, res)
+
+ var foo string
+ var bar int64
+ env.vmk.prmk.GetString(ctx, "gno.land/r/test.foo.string", &foo)
+ env.vmk.prmk.GetInt64(ctx, "gno.land/r/test.bar.int64", &bar)
+ assert.Equal(t, "foo2", foo)
+ assert.Equal(t, int64(1337), bar)
+}
+
// Assign admin as OrigCaller on deploying the package.
func TestVMKeeperOrigCallerInit(t *testing.T) {
env := setupTestEnv()
- ctx := env.ctx
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
// Give "addr1" some gnots.
addr := crypto.AddressFromPreimage([]byte("addr1"))
@@ -306,8 +403,8 @@ func TestVMKeeperOrigCallerInit(t *testing.T) {
assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString)))
// Create test package.
- files := []*std.MemFile{
- {"init.gno", `
+ files := []*gnovm.MemFile{
+ {Name: "init.gno", Body: `
package test
import "std"
@@ -315,7 +412,7 @@ import "std"
var admin std.Address
func init() {
- admin = std.GetOrigCaller()
+ admin = std.GetOrigCaller()
}
func Echo(msg string) string {
@@ -350,15 +447,15 @@ func GetAdmin() string {
// Call Run without imports, without variables.
func TestVMKeeperRunSimple(t *testing.T) {
env := setupTestEnv()
- ctx := env.ctx
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
// Give "addr1" some gnots.
addr := crypto.AddressFromPreimage([]byte("addr1"))
acc := env.acck.NewAccountWithAddress(ctx, addr)
env.acck.SetAccount(ctx, acc)
- files := []*std.MemFile{
- {"script.gno", `
+ files := []*gnovm.MemFile{
+ {Name: "script.gno", Body: `
package main
func main() {
@@ -389,15 +486,15 @@ func TestVMKeeperRunImportStdlibsColdStdlibLoad(t *testing.T) {
func testVMKeeperRunImportStdlibs(t *testing.T, env testEnv) {
t.Helper()
- ctx := env.ctx
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
// Give "addr1" some gnots.
addr := crypto.AddressFromPreimage([]byte("addr1"))
acc := env.acck.NewAccountWithAddress(ctx, addr)
env.acck.SetAccount(ctx, acc)
- files := []*std.MemFile{
- {"script.gno", `
+ files := []*gnovm.MemFile{
+ {Name: "script.gno", Body: `
package main
import "std"
@@ -419,7 +516,7 @@ func main() {
func TestNumberOfArgsError(t *testing.T) {
env := setupTestEnv()
- ctx := env.ctx
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
// Give "addr1" some gnots.
addr := crypto.AddressFromPreimage([]byte("addr1"))
@@ -429,7 +526,7 @@ func TestNumberOfArgsError(t *testing.T) {
assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString)))
// Create test package.
- files := []*std.MemFile{
+ files := []*gnovm.MemFile{
{
Name: "test.gno",
Body: `package test
@@ -455,3 +552,59 @@ func Echo(msg string) string {
},
)
}
+
+func TestVMKeeperReinitialize(t *testing.T) {
+ env := setupTestEnv()
+ ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
+
+ // Give "addr1" some gnots.
+ addr := crypto.AddressFromPreimage([]byte("addr1"))
+ acc := env.acck.NewAccountWithAddress(ctx, addr)
+ env.acck.SetAccount(ctx, acc)
+ env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString))
+ assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString)))
+
+ // Create test package.
+ files := []*gnovm.MemFile{
+ {Name: "init.gno", Body: `
+package test
+
+func Echo(msg string) string {
+ return "echo:"+msg
+}`},
+ }
+ pkgPath := "gno.land/r/test"
+ msg1 := NewMsgAddPackage(addr, pkgPath, files)
+ err := env.vmk.AddPackage(ctx, msg1)
+ require.NoError(t, err)
+
+ // Run Echo function.
+ msg2 := NewMsgCall(addr, nil, pkgPath, "Echo", []string{"hello world"})
+ res, err := env.vmk.Call(ctx, msg2)
+ require.NoError(t, err)
+ assert.Equal(t, `("echo:hello world" string)`+"\n\n", res)
+
+ // Clear out gnovm and reinitialize.
+ env.vmk.gnoStore = nil
+ mcw := env.ctx.MultiStore().MultiCacheWrap()
+ env.vmk.Initialize(log.NewNoopLogger(), mcw)
+ mcw.MultiWrite()
+
+ // Run echo again, and it should still work.
+ res, err = env.vmk.Call(ctx, msg2)
+ require.NoError(t, err)
+ assert.Equal(t, `("echo:hello world" string)`+"\n\n", res)
+}
+
+func Test_loadStdlibPackage(t *testing.T) {
+ mdb := memdb.NewMemDB()
+ cs := dbadapter.StoreConstructor(mdb, types.StoreOptions{})
+
+ gs := gnolang.NewStore(nil, cs, cs)
+ assert.PanicsWithValue(t, `failed loading stdlib "notfound": does not exist`, func() {
+ loadStdlibPackage("notfound", "./testdata", gs)
+ })
+ assert.PanicsWithValue(t, `failed loading stdlib "emptystdlib": not a valid MemPackage`, func() {
+ loadStdlibPackage("emptystdlib", "./testdata", gs)
+ })
+}
diff --git a/gno.land/pkg/sdk/vm/msg_test.go b/gno.land/pkg/sdk/vm/msg_test.go
new file mode 100644
index 00000000000..684dc21e9f2
--- /dev/null
+++ b/gno.land/pkg/sdk/vm/msg_test.go
@@ -0,0 +1,272 @@
+package vm
+
+import (
+ "testing"
+
+ "github.com/gnolang/gno/gnovm"
+ "github.com/gnolang/gno/tm2/pkg/crypto"
+ "github.com/gnolang/gno/tm2/pkg/std"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMsgAddPackage_ValidateBasic(t *testing.T) {
+ t.Parallel()
+
+ creator := crypto.AddressFromPreimage([]byte("addr1"))
+ pkgName := "test"
+ pkgPath := "gno.land/r/namespace/test"
+ files := []*gnovm.MemFile{
+ {
+ Name: "test.gno",
+ Body: `package test
+ func Echo() string {return "hello world"}`,
+ },
+ }
+
+ tests := []struct {
+ name string
+ msg MsgAddPackage
+ expectSignBytes string
+ expectErr error
+ }{
+ {
+ name: "valid message",
+ msg: NewMsgAddPackage(creator, pkgPath, files),
+ expectSignBytes: `{"creator":"g14ch5q26mhx3jk5cxl88t278nper264ces4m8nt","deposit":"",` +
+ `"package":{"files":[{"body":"package test\n\t\tfunc Echo() string {return \"hello world\"}",` +
+ `"name":"test.gno"}],"name":"test","path":"gno.land/r/namespace/test"}}`,
+ expectErr: nil,
+ },
+ {
+ name: "missing creator address",
+ msg: MsgAddPackage{
+ Creator: crypto.Address{},
+ Package: &gnovm.MemPackage{
+ Name: pkgName,
+ Path: pkgPath,
+ Files: files,
+ },
+ Deposit: std.Coins{std.Coin{
+ Denom: "ugnot",
+ Amount: 1000,
+ }},
+ },
+ expectErr: std.InvalidAddressError{},
+ },
+ {
+ name: "missing package path",
+ msg: MsgAddPackage{
+ Creator: creator,
+ Package: &gnovm.MemPackage{
+ Name: pkgName,
+ Path: "",
+ Files: files,
+ },
+ Deposit: std.Coins{std.Coin{
+ Denom: "ugnot",
+ Amount: 1000,
+ }},
+ },
+ expectErr: InvalidPkgPathError{},
+ },
+ {
+ name: "invalid deposit coins",
+ msg: MsgAddPackage{
+ Creator: creator,
+ Package: &gnovm.MemPackage{
+ Name: pkgName,
+ Path: pkgPath,
+ Files: files,
+ },
+ Deposit: std.Coins{std.Coin{
+ Denom: "ugnot",
+ Amount: -1000, // invalid amount
+ }},
+ },
+ expectErr: std.InvalidCoinsError{},
+ },
+ }
+
+ for _, tc := range tests {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ if err := tc.msg.ValidateBasic(); err != nil {
+ assert.ErrorIs(t, err, tc.expectErr)
+ } else {
+ assert.Equal(t, tc.expectSignBytes, string(tc.msg.GetSignBytes()))
+ }
+ })
+ }
+}
+
+func TestMsgCall_ValidateBasic(t *testing.T) {
+ t.Parallel()
+
+ caller := crypto.AddressFromPreimage([]byte("addr1"))
+ pkgPath := "gno.land/r/namespace/test"
+ funcName := "MyFunction"
+ args := []string{"arg1", "arg2"}
+
+ tests := []struct {
+ name string
+ msg MsgCall
+ expectSignBytes string
+ expectErr error
+ }{
+ {
+ name: "valid message",
+ msg: NewMsgCall(caller, std.NewCoins(std.NewCoin("ugnot", 1000)), pkgPath, funcName, args),
+ expectSignBytes: `{"args":["arg1","arg2"],"caller":"g14ch5q26mhx3jk5cxl88t278nper264ces4m8nt",` +
+ `"func":"MyFunction","pkg_path":"gno.land/r/namespace/test","send":"1000ugnot"}`,
+ expectErr: nil,
+ },
+ {
+ name: "invalid caller address",
+ msg: MsgCall{
+ Caller: crypto.Address{},
+ PkgPath: pkgPath,
+ Func: funcName,
+ Args: args,
+ Send: std.Coins{std.Coin{
+ Denom: "ugnot",
+ Amount: 1000,
+ }},
+ },
+ expectErr: std.InvalidAddressError{},
+ },
+ {
+ name: "missing package path",
+ msg: MsgCall{
+ Caller: caller,
+ PkgPath: "",
+ Func: funcName,
+ Args: args,
+ Send: std.Coins{std.Coin{
+ Denom: "ugnot",
+ Amount: 1000,
+ }},
+ },
+ expectErr: InvalidPkgPathError{},
+ },
+ {
+ name: "pkgPath should not be a realm path",
+ msg: MsgCall{
+ Caller: caller,
+ PkgPath: "gno.land/p/namespace/test", // this is not a valid realm path
+ Func: funcName,
+ Args: args,
+ Send: std.Coins{std.Coin{
+ Denom: "ugnot",
+ Amount: 1000,
+ }},
+ },
+ expectErr: InvalidPkgPathError{},
+ },
+ {
+ name: "missing function name to call",
+ msg: MsgCall{
+ Caller: caller,
+ PkgPath: pkgPath,
+ Func: "",
+ Args: args,
+ Send: std.Coins{std.Coin{
+ Denom: "ugnot",
+ Amount: 1000,
+ }},
+ },
+ expectErr: InvalidExprError{},
+ },
+ }
+
+ for _, tc := range tests {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ if err := tc.msg.ValidateBasic(); err != nil {
+ assert.ErrorIs(t, err, tc.expectErr)
+ } else {
+ assert.Equal(t, tc.expectSignBytes, string(tc.msg.GetSignBytes()))
+ }
+ })
+ }
+}
+
+func TestMsgRun_ValidateBasic(t *testing.T) {
+ t.Parallel()
+
+ caller := crypto.AddressFromPreimage([]byte("addr1"))
+ pkgName := "main"
+ pkgPath := "gno.land/r/" + caller.String() + "/run"
+ pkgFiles := []*gnovm.MemFile{
+ {
+ Name: "main.gno",
+ Body: `package main
+ func Echo() string {return "hello world"}`,
+ },
+ }
+
+ tests := []struct {
+ name string
+ msg MsgRun
+ expectSignBytes string
+ expectErr error
+ }{
+ {
+ name: "valid message",
+ msg: NewMsgRun(caller, std.NewCoins(std.NewCoin("ugnot", 1000)), pkgFiles),
+ expectSignBytes: `{"caller":"g14ch5q26mhx3jk5cxl88t278nper264ces4m8nt",` +
+ `"package":{"files":[{"body":"package main\n\t\tfunc Echo() string {return \"hello world\"}",` +
+ `"name":"main.gno"}],"name":"main","path":""},` +
+ `"send":"1000ugnot"}`,
+ expectErr: nil,
+ },
+ {
+ name: "invalid caller address",
+ msg: MsgRun{
+ Caller: crypto.Address{},
+ Package: &gnovm.MemPackage{
+ Name: pkgName,
+ Path: pkgPath,
+ Files: pkgFiles,
+ },
+ Send: std.Coins{std.Coin{
+ Denom: "ugnot",
+ Amount: 1000,
+ }},
+ },
+ expectErr: std.InvalidAddressError{},
+ },
+ {
+ name: "invalid package path",
+ msg: MsgRun{
+ Caller: caller,
+ Package: &gnovm.MemPackage{
+ Name: pkgName,
+ Path: "gno.land/r/namespace/test", // this is not a valid run path
+ Files: pkgFiles,
+ },
+ Send: std.Coins{std.Coin{
+ Denom: "ugnot",
+ Amount: 1000,
+ }},
+ },
+ expectErr: InvalidPkgPathError{},
+ },
+ }
+
+ for _, tc := range tests {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ if err := tc.msg.ValidateBasic(); err != nil {
+ assert.ErrorIs(t, err, tc.expectErr)
+ } else {
+ assert.Equal(t, tc.expectSignBytes, string(tc.msg.GetSignBytes()))
+ }
+ })
+ }
+}
diff --git a/gno.land/pkg/sdk/vm/msgs.go b/gno.land/pkg/sdk/vm/msgs.go
index d650c23f382..38f35ab7110 100644
--- a/gno.land/pkg/sdk/vm/msgs.go
+++ b/gno.land/pkg/sdk/vm/msgs.go
@@ -4,6 +4,7 @@ import (
"fmt"
"strings"
+ "github.com/gnolang/gno/gnovm"
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
"github.com/gnolang/gno/tm2/pkg/amino"
"github.com/gnolang/gno/tm2/pkg/crypto"
@@ -16,25 +17,25 @@ import (
// MsgAddPackage - create and initialize new package
type MsgAddPackage struct {
- Creator crypto.Address `json:"creator" yaml:"creator"`
- Package *std.MemPackage `json:"package" yaml:"package"`
- Deposit std.Coins `json:"deposit" yaml:"deposit"`
+ Creator crypto.Address `json:"creator" yaml:"creator"`
+ Package *gnovm.MemPackage `json:"package" yaml:"package"`
+ Deposit std.Coins `json:"deposit" yaml:"deposit"`
}
var _ std.Msg = MsgAddPackage{}
// NewMsgAddPackage - upload a package with files.
-func NewMsgAddPackage(creator crypto.Address, pkgPath string, files []*std.MemFile) MsgAddPackage {
+func NewMsgAddPackage(creator crypto.Address, pkgPath string, files []*gnovm.MemFile) MsgAddPackage {
var pkgName string
for _, file := range files {
if strings.HasSuffix(file.Name, ".gno") {
- pkgName = string(gno.PackageNameFromFileBody(file.Name, file.Body))
+ pkgName = string(gno.MustPackageNameFromFileBody(file.Name, file.Body))
break
}
}
return MsgAddPackage{
Creator: creator,
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: pkgName,
Path: pkgPath,
Files: files,
@@ -145,17 +146,17 @@ func (msg MsgCall) GetReceived() std.Coins {
// MsgRun - executes arbitrary Gno code.
type MsgRun struct {
- Caller crypto.Address `json:"caller" yaml:"caller"`
- Send std.Coins `json:"send" yaml:"send"`
- Package *std.MemPackage `json:"package" yaml:"package"`
+ Caller crypto.Address `json:"caller" yaml:"caller"`
+ Send std.Coins `json:"send" yaml:"send"`
+ Package *gnovm.MemPackage `json:"package" yaml:"package"`
}
var _ std.Msg = MsgRun{}
-func NewMsgRun(caller crypto.Address, send std.Coins, files []*std.MemFile) MsgRun {
+func NewMsgRun(caller crypto.Address, send std.Coins, files []*gnovm.MemFile) MsgRun {
for _, file := range files {
if strings.HasSuffix(file.Name, ".gno") {
- pkgName := string(gno.PackageNameFromFileBody(file.Name, file.Body))
+ pkgName := string(gno.MustPackageNameFromFileBody(file.Name, file.Body))
if pkgName != "main" {
panic("package name should be 'main'")
}
@@ -164,7 +165,7 @@ func NewMsgRun(caller crypto.Address, send std.Coins, files []*std.MemFile) MsgR
return MsgRun{
Caller: caller,
Send: send,
- Package: &std.MemPackage{
+ Package: &gnovm.MemPackage{
Name: "main",
Path: "", // auto set by the handler
Files: files,
@@ -185,8 +186,8 @@ func (msg MsgRun) ValidateBasic() error {
}
// Force memPkg path to the reserved run path.
- wantPath := "gno.land/r/" + msg.Caller.String() + "/run"
- if path := msg.Package.Path; path != "" && path != wantPath {
+ wantSuffix := "/r/" + msg.Caller.String() + "/run"
+ if path := msg.Package.Path; path != "" && !strings.HasSuffix(path, wantSuffix) {
return ErrInvalidPkgPath(fmt.Sprintf("invalid pkgpath for MsgRun: %q", path))
}
diff --git a/gno.land/pkg/sdk/vm/package.go b/gno.land/pkg/sdk/vm/package.go
index e62a7b53928..0359061ccea 100644
--- a/gno.land/pkg/sdk/vm/package.go
+++ b/gno.land/pkg/sdk/vm/package.go
@@ -1,6 +1,7 @@
package vm
import (
+ "github.com/gnolang/gno/gnovm"
"github.com/gnolang/gno/tm2/pkg/amino"
"github.com/gnolang/gno/tm2/pkg/std"
)
@@ -11,6 +12,7 @@ var Package = amino.RegisterPackage(amino.NewPackage(
amino.GetCallersDirname(),
).WithDependencies(
std.Package,
+ gnovm.Package,
).WithTypes(
MsgCall{}, "m_call",
MsgRun{}, "m_run",
@@ -18,6 +20,7 @@ var Package = amino.RegisterPackage(amino.NewPackage(
// errors
InvalidPkgPathError{}, "InvalidPkgPathError",
+ PkgExistError{}, "PkgExistError",
InvalidStmtError{}, "InvalidStmtError",
InvalidExprError{}, "InvalidExprError",
TypeCheckError{}, "TypeCheckError",
diff --git a/gno.land/pkg/sdk/vm/params.go b/gno.land/pkg/sdk/vm/params.go
new file mode 100644
index 00000000000..248fb8a81fb
--- /dev/null
+++ b/gno.land/pkg/sdk/vm/params.go
@@ -0,0 +1,20 @@
+package vm
+
+import "github.com/gnolang/gno/tm2/pkg/sdk"
+
+const (
+ sysUsersPkgParamPath = "gno.land/r/sys/params.sys.users_pkgpath.string"
+ chainDomainParamPath = "gno.land/r/sys/params.chain_domain.string"
+)
+
+func (vm *VMKeeper) getChainDomainParam(ctx sdk.Context) string {
+ chainDomain := "gno.land" // default
+ vm.prmk.GetString(ctx, chainDomainParamPath, &chainDomain)
+ return chainDomain
+}
+
+func (vm *VMKeeper) getSysUsersPkgParam(ctx sdk.Context) string {
+ var sysUsersPkg string
+ vm.prmk.GetString(ctx, sysUsersPkgParamPath, &sysUsersPkg)
+ return sysUsersPkg
+}
diff --git a/gno.land/pkg/sdk/vm/testdata/emptystdlib/README b/gno.land/pkg/sdk/vm/testdata/emptystdlib/README
new file mode 100644
index 00000000000..e4454ed67f8
--- /dev/null
+++ b/gno.land/pkg/sdk/vm/testdata/emptystdlib/README
@@ -0,0 +1 @@
+see keeper_test.go
diff --git a/gno.land/pkg/sdk/vm/vm.proto b/gno.land/pkg/sdk/vm/vm.proto
index aa0be4f6e14..efaf025e431 100644
--- a/gno.land/pkg/sdk/vm/vm.proto
+++ b/gno.land/pkg/sdk/vm/vm.proto
@@ -5,6 +5,7 @@ option go_package = "github.com/gnolang/gno/gno.land/pkg/sdk/vm/pb";
// imports
import "github.com/gnolang/gno/tm2/pkg/std/std.proto";
+import "github.com/gnolang/gno/gnovm/gnovm.proto";
// messages
message m_call {
@@ -18,18 +19,21 @@ message m_call {
message m_run {
string caller = 1;
string send = 2;
- std.MemPackage package = 3;
+ gnovm.MemPackage package = 3;
}
message m_addpkg {
string creator = 1;
- std.MemPackage package = 2;
+ gnovm.MemPackage package = 2;
string deposit = 3;
}
message InvalidPkgPathError {
}
+message PkgExistError {
+}
+
message InvalidStmtError {
}
@@ -37,5 +41,8 @@ message InvalidExprError {
}
message TypeCheckError {
- repeated string errors = 1 [json_name = "Errors"];
+ repeated string errors = 1;
+}
+
+message UnauthorizedUserError {
}
\ No newline at end of file
diff --git a/gnovm/Makefile b/gnovm/Makefile
index 5ff3af9c253..d724ffbb6a2 100644
--- a/gnovm/Makefile
+++ b/gnovm/Makefile
@@ -54,17 +54,37 @@ lint:
.PHONY: fmt
fmt:
- go run ./cmd/gno fmt $(GNOFMT_FLAGS) ./stdlibs/...
+ go run ./cmd/gno fmt $(GNOFMT_FLAGS) ./stdlibs/... ./tests/stdlibs/...
$(rundep) mvdan.cc/gofumpt $(GOFMT_FLAGS) .
.PHONY: imports
imports:
$(rundep) golang.org/x/tools/cmd/goimports $(GOIMPORTS_FLAGS) .
+# Benchmarking the VM's opcode and storage access
+.PHONY: opcode storage build_opcode build_storage
+build.bench.opcode:
+ go build -tags "benchmarkingops" -o build/gnobench ./cmd/benchops
+
+build.bench.storage:
+ go build -tags "benchmarkingstorage" -o build/gnobench ./cmd/benchops
+
+# Extract the latest commit hash
+COMMIT_HASH := $(shell git rev-parse --short=7 HEAD)
+
+# Run target
+run.bench.opcode: build.bench.opcode
+ ./build/gnobench -out opcode_results_$(COMMIT_HASH).csv
+
+# Run target
+run.bench.storage: build.bench.storage
+ ./build/gnobench -out store_results_$(COMMIT_HASH).csv
+
+
########################################
# Test suite
.PHONY: test
-test: _test.cmd _test.pkg _test.gnolang
+test: _test.cmd _test.pkg _test.stdlibs
.PHONY: _test.cmd
_test.cmd:
@@ -92,20 +112,12 @@ test.cmd.coverage_view: test.cmd.coverage
_test.pkg:
go test ./pkg/... $(GOTEST_FLAGS)
-.PHONY: _test.gnolang
-_test.gnolang: _test.gnolang.native _test.gnolang.stdlibs _test.gnolang.realm _test.gnolang.pkg0 _test.gnolang.pkg1 _test.gnolang.pkg2 _test.gnolang.other
-_test.gnolang.other:; go test tests/*.go -run "(TestFileStr|TestSelectors)" $(GOTEST_FLAGS)
-_test.gnolang.realm:; go test tests/*.go -run "TestFiles/^zrealm" $(GOTEST_FLAGS)
-_test.gnolang.pkg0:; go test tests/*.go -run "TestStdlibs/(bufio|crypto|encoding|errors|internal|io|math|sort|std|strconv|strings|testing|unicode)" $(GOTEST_FLAGS)
-_test.gnolang.pkg1:; go test tests/*.go -run "TestStdlibs/regexp" $(GOTEST_FLAGS)
-_test.gnolang.pkg2:; go test tests/*.go -run "TestStdlibs/bytes" $(GOTEST_FLAGS)
-_test.gnolang.native:; go test tests/*.go -test.short -run "TestFilesNative/" $(GOTEST_FLAGS)
-_test.gnolang.stdlibs:; go test tests/*.go -test.short -run 'TestFiles$$/' $(GOTEST_FLAGS)
-_test.gnolang.native.sync:; go test tests/*.go -test.short -run "TestFilesNative/" --update-golden-tests $(GOTEST_FLAGS)
-_test.gnolang.stdlibs.sync:; go test tests/*.go -test.short -run 'TestFiles$$/' --update-golden-tests $(GOTEST_FLAGS)
-# NOTE: challenges are current GnoVM bugs which are supposed to fail.
-# If any of these tests pass, it should be moved to a normal test.
-_test.gnolang.challenges:; go test tests/*.go -test.short -run 'TestChallenges$$/' $(GOTEST_FLAGS)
+.PHONY: _test.stdlibs
+_test.stdlibs:
+ go run ./cmd/gno test -v ./stdlibs/...
+
+_test.filetest:;
+ go test pkg/gnolang/files_test.go -test.short -run 'TestFiles$$/' $(GOTEST_FLAGS)
########################################
# Code gen
diff --git a/gnovm/README.md b/gnovm/README.md
index 91419746cfa..2fe4345c367 100644
--- a/gnovm/README.md
+++ b/gnovm/README.md
@@ -4,7 +4,7 @@ GnoVM is a virtual machine that interprets Gnolang, a custom version of Golang o
It works with Tendermint2 and enables smarter, more modular, and transparent appchains with embedded smart-contracts.
It can be used in TendermintCore, forks, and non-Cosmos blockchains.
-Read the ["Intro to Gnoland"](https://test3.gno.land/r/gnoland/blog:p/intro) blogpost.
+Read the ["Intro to Gnoland"](https://gno.land/r/gnoland/blog:p/intro) blogpost.
This folder focuses on the VM, language, stdlibs, tests, and tools, independent of the blockchain.
This enables non-web3 developers to contribute without requiring an understanding of the broader context.
diff --git a/gnovm/cmd/benchops/README.md b/gnovm/cmd/benchops/README.md
new file mode 100644
index 00000000000..d2eda4ce99d
--- /dev/null
+++ b/gnovm/cmd/benchops/README.md
@@ -0,0 +1,90 @@
+# `gnobench` the time consumed for GnoVM OpCode execution and store access
+
+`gnobench` benchmarks the time consumed for each VM CPU OpCode and persistent access to the store, including marshalling and unmarshalling of realm objects.
+
+## Usage
+
+### Simple mode
+
+The benchmark only involves the GnoVM and the persistent store. It benchmarks the bare minimum components, and the results are isolated from other components. We use standardize gno contract to perform the benchmarking.
+
+This mode is the best for benchmarking each major release and/or changes in GnoVM.
+
+ make opcode
+ make storage
+
+### Production mode
+
+It benchmarks the node in the production environment with minimum overhead.
+We can not only benchmark with standardize the contract but also capture the live usage in production environment.
+It gives us a complete picture of the node perform.
+
+
+ 1. Build the production node with benchmarking flags:
+
+ `go build -tags "benchmarkingstorage benchmarkingops" gno.land/cmd/gnoland`
+
+ 2. Run the node in the production environment. It will dump benchmark data to a benchmarks.bin file.
+
+ 3. call the realm contracts at `gno.land/r/x/benchmark/opcodes` and `gno.land/r/x/benchmark/storage`
+
+ 4. Stop the server after the benchmarking session is complete.
+
+ 5. Run the following command to convert the binary dump:
+
+ `gnobench -bin path_to_benchmarks.bin`
+
+ it converts the binary dump to results.csv and results_stats.csv.
+
+
+## Results
+
+The benchmarking results are stored in two files:
+ 1. The raw results are saved in results.csv.
+
+ | Operation | Elapsed Time | Disk IO Bytes |
+ |-----------------|--------------|---------------|
+ | OpEval | 40333 | 0 |
+ | OpPopBlock | 208 | 0 |
+ | OpHalt | 167 | 0 |
+ | OpEval | 500 | 0 |
+ | OpInterfaceType | 458 | 0 |
+ | OpPopBlock | 166 | 0 |
+ | OpHalt | 125 | 0 |
+ | OpInterfaceType | 21125 | 0 |
+ | OpEval | 541 | 0 |
+ | OpEval | 209 | 0 |
+ | OpInterfaceType | 334 | 0 |
+
+
+
+ 2. The averages and standard deviations are summarized in results_stats.csv.
+
+ | Operation | Avg Time | Avg Size | Time Std Dev | Count |
+|----------------|----------|----------|--------------|-------|
+| OpAdd | 101 | 0 | 45 | 300 |
+| OpAddAssign | 309 | 0 | 1620 | 100 |
+| OpArrayLit | 242 | 0 | 170 | 700 |
+| OpArrayType | 144 | 0 | 100 | 714 |
+| OpAssign | 136 | 0 | 95 | 2900 |
+| OpBand | 92 | 0 | 30 | 100 |
+| OpBandAssign | 127 | 0 | 62 | 100 |
+| OpBandn | 97 | 0 | 54 | 100 |
+| OpBandnAssign | 125 | 0 | 113 | 100 |
+| OpBinary1 | 128 | 0 | 767 | 502 |
+| OpBody | 127 | 0 | 145 | 13700 |
+
+## Design consideration
+
+### Minimum Overhead and Footprint
+
+- Constant build flags enable benchmarking.
+- Encode operations and measurements in binary.
+- Dump to a local file in binary.
+- No logging, printout, or network access involved.
+
+### Accurate
+
+- Pause the timer for storage access while performing VM opcode benchmarking.
+- Measure each OpCode execution in nanoseconds.
+- Store access includes the duration for Amino marshalling and unmarshalling.
diff --git a/gnovm/cmd/benchops/main.go b/gnovm/cmd/benchops/main.go
new file mode 100644
index 00000000000..63535949f3b
--- /dev/null
+++ b/gnovm/cmd/benchops/main.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+ "flag"
+ "log"
+ "os"
+ "path/filepath"
+
+ bm "github.com/gnolang/gno/gnovm/pkg/benchops"
+)
+
+var (
+ outFlag = flag.String("out", "results.csv", "the out put file")
+ benchFlag = flag.String("bench", "./pkg/benchops/gno", "the path to the benchmark contract")
+ binFlag = flag.String("bin", "", "interpret the existing benchmarking file.")
+)
+
+// We dump the benchmark in bytes for speed and minimal overhead.
+const tmpFile = "benchmark.bin"
+
+func main() {
+ flag.Parse()
+ if *binFlag != "" {
+ binFile, err := filepath.Abs(*binFlag)
+ if err != nil {
+ log.Fatal("unable to get absolute path for the file", err)
+ }
+ stats(binFile)
+ return
+ }
+ bm.Init(tmpFile)
+ bstore := benchmarkDiskStore()
+ defer bstore.Delete()
+
+ dir, err := filepath.Abs(*benchFlag)
+ if err != nil {
+ log.Fatal("unable to get absolute path for storage directory.", err)
+ }
+
+ // load stdlibs
+ loadStdlibs(bstore)
+
+ if bm.OpsEnabled {
+ benchmarkOpCodes(bstore.gnoStore, dir)
+ }
+ if bm.StorageEnabled {
+ benchmarkStorage(bstore, dir)
+ }
+ bm.Finish()
+ stats(tmpFile)
+ err = os.Remove(tmpFile)
+ if err != nil {
+ log.Printf("Error removing tmp file: %v", err)
+ }
+}
diff --git a/gnovm/cmd/benchops/opcode_test.go b/gnovm/cmd/benchops/opcode_test.go
new file mode 100644
index 00000000000..17852186448
--- /dev/null
+++ b/gnovm/cmd/benchops/opcode_test.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+ "testing"
+
+ gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestLoadOpcodesPackage(t *testing.T) {
+ dir := "../../pkg/benchops/gno/opcodes"
+ diskStore := benchmarkDiskStore()
+ gstore := diskStore.gnoStore
+ t.Cleanup(func() { diskStore.Delete() })
+ pv := addPackage(gstore, dir, opcodesPkgPath)
+ pb := pv.GetBlock(gstore)
+
+ assert := assert.New(t)
+ require := require.New(t)
+
+ declTypes := []string{
+ "foo",
+ "dog",
+ "foofighter",
+ }
+ for i := 0; i < len(declTypes); i++ {
+ tv := pb.Values[i]
+ v, ok := tv.V.(gno.TypeValue)
+ require.True(ok, "it should be a TypeValue")
+ dtv, ok2 := v.Type.(*gno.DeclaredType)
+ tn := declTypes[i]
+
+ require.True(ok2, "it should be a DeclaredType")
+ assert.Equal(tn, string(dtv.Name), "the declared type name should be "+tn)
+ }
+
+ // These are the functions used to benchmark the OpCode in the benchmarking contract.
+ // We call each to benchmark a group of OpCodes.
+ funcValues := []string{
+ "ExprOps",
+ "OpDecl",
+ "OpEvalInt",
+ "OpEvalFloat",
+ "StmtOps",
+ "ControlOps",
+ "OpDefer",
+ "OpUnary",
+ "OpBinary",
+ "OpLor",
+ "OpLand",
+ "OpPanic",
+ "OpTypeSwitch",
+ "OpCallDeferNativeBody",
+ "OpRange",
+ "OpForLoop",
+ "OpTypes",
+ "OpOpValues",
+ }
+
+ for i := 3; i < 3+len(funcValues); i++ {
+ j := i - 3
+ tv := pb.Values[i]
+ fv, ok := tv.V.(*gno.FuncValue)
+ require.True(ok, "it should be a FuncValue")
+ fn := funcValues[j]
+ assert.Equal(fn, string(fv.Name), "the declared type name should be "+fn)
+ }
+}
diff --git a/gnovm/cmd/benchops/run.go b/gnovm/cmd/benchops/run.go
new file mode 100644
index 00000000000..e01fbc1cb6b
--- /dev/null
+++ b/gnovm/cmd/benchops/run.go
@@ -0,0 +1,143 @@
+package main
+
+import (
+ "io"
+ "path/filepath"
+ "strings"
+
+ "github.com/gnolang/gno/gnovm/pkg/gnoenv"
+ gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
+ "github.com/gnolang/gno/gnovm/stdlibs"
+ osm "github.com/gnolang/gno/tm2/pkg/os"
+)
+
+const (
+ opcodesPkgPath = "gno.land/r/x/benchmark/opcodes"
+ rounds = 1000
+)
+
+func benchmarkOpCodes(bstore gno.Store, dir string) {
+ opcodesPkgDir := filepath.Join(dir, "opcodes")
+
+ pv := addPackage(bstore, opcodesPkgDir, opcodesPkgPath)
+ for i := 0; i < rounds; i++ {
+ callOpsBench(bstore, pv)
+ }
+}
+
+func callOpsBench(bstore gno.Store, pv *gno.PackageValue) {
+ // start
+ pb := pv.GetBlock(bstore)
+ for _, tv := range pb.Values {
+ if fv, ok := tv.V.(*gno.FuncValue); ok {
+ cx := gno.Call(fv.Name)
+ callFunc(bstore, pv, cx)
+ }
+ }
+}
+
+const storagePkgPath = "gno.land/r/x/benchmark/storage"
+
+func benchmarkStorage(bstore BenchStore, dir string) {
+ gs := bstore.gnoStore
+ avlPkgDir := filepath.Join(dir, "avl")
+ addPackage(gs, avlPkgDir, "gno.land/p/demo/avl")
+
+ storagePkgDir := filepath.Join(dir, "storage")
+ pv := addPackage(gs, storagePkgDir, storagePkgPath)
+ benchStoreSet(bstore, pv)
+ benchStoreGet(bstore, pv)
+}
+
+func benchStoreSet(bstore BenchStore, pv *gno.PackageValue) {
+ title := "1KB content"
+ content := strings.Repeat("a", 1024)
+
+ // in forum.gno: func AddPost(title, content string)
+ // one AddPost will be added to three different boards in the forum.gno contract
+
+ for i := 0; i < rounds; i++ {
+ cx := gno.Call("AddPost", gno.Str(title), gno.Str(content))
+ callFunc(bstore.gnoStore, pv, cx)
+ bstore.Write()
+ bstore.gnoStore.ClearObjectCache()
+ }
+}
+
+func benchStoreGet(bstore BenchStore, pv *gno.PackageValue) {
+ // in forum.gno: func GetPost(boardId, postId int) string in forum.gno
+ // there are three different boards on the benchmarking forum contract
+ for i := 0; i < 3; i++ {
+ for j := 0; j < rounds; j++ {
+ cx := gno.Call("GetPost", gno.X(i), gno.X(j))
+ callFunc(bstore.gnoStore, pv, cx)
+ bstore.Write()
+ bstore.gnoStore.ClearObjectCache()
+ }
+ }
+}
+
+func callFunc(gstore gno.Store, pv *gno.PackageValue, cx gno.Expr) []gno.TypedValue {
+ m := gno.NewMachineWithOptions(
+ gno.MachineOptions{
+ PkgPath: pv.PkgPath,
+ Output: io.Discard,
+ Store: gstore,
+ })
+
+ defer m.Release()
+
+ m.SetActivePackage(pv)
+ return m.Eval(cx)
+}
+
+// addPacakge
+
+func addPackage(gstore gno.Store, dir string, pkgPath string) *gno.PackageValue {
+ // load benchmark contract
+ m := gno.NewMachineWithOptions(
+ gno.MachineOptions{
+ PkgPath: "",
+ Output: io.Discard,
+ Store: gstore,
+ })
+ defer m.Release()
+
+ memPkg := gno.MustReadMemPackage(dir, pkgPath)
+
+ // pare the file, create pn, pv and save the values in m.store
+ _, pv := m.RunMemPackage(memPkg, true)
+
+ return pv
+}
+
+// load stdlibs
+func loadStdlibs(bstore BenchStore) {
+ // copied from vm/builtin.go
+ getPackage := func(pkgPath string, newStore gno.Store) (pn *gno.PackageNode, pv *gno.PackageValue) {
+ stdlibDir := filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs")
+ stdlibPath := filepath.Join(stdlibDir, pkgPath)
+ if !osm.DirExists(stdlibPath) {
+ // does not exist.
+ return nil, nil
+ }
+
+ memPkg := gno.MustReadMemPackage(stdlibPath, pkgPath)
+ if memPkg.IsEmpty() {
+ // no gno files are present, skip this package
+ return nil, nil
+ }
+
+ m2 := gno.NewMachineWithOptions(gno.MachineOptions{
+ PkgPath: "gno.land/r/stdlibs/" + pkgPath,
+ // PkgPath: pkgPath,
+ Output: io.Discard,
+ Store: newStore,
+ })
+ defer m2.Release()
+ return m2.RunMemPackage(memPkg, true)
+ }
+
+ bstore.gnoStore.SetPackageGetter(getPackage)
+ bstore.gnoStore.SetNativeResolver(stdlibs.NativeResolver)
+}
diff --git a/gnovm/cmd/benchops/stats.go b/gnovm/cmd/benchops/stats.go
new file mode 100644
index 00000000000..97f36125750
--- /dev/null
+++ b/gnovm/cmd/benchops/stats.go
@@ -0,0 +1,188 @@
+package main
+
+import (
+ "encoding/binary"
+ "fmt"
+ "math"
+ "os"
+ "sort"
+ "strings"
+ "sync"
+
+ bm "github.com/gnolang/gno/gnovm/pkg/benchops"
+ gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
+)
+
+type codeStats struct {
+ codeName string
+ avgTime int64
+ avgSize int64
+ timeStdDev int64
+ count int
+}
+
+type codeRecord struct {
+ codeName string
+ elapsed uint32
+ size uint32
+}
+
+// It reads binary record, calcuate and output the statistics of operations
+func stats(binFile string) {
+ in, err := os.Open(binFile)
+ if err != nil {
+ panic("could not create benchmark file: " + err.Error())
+ }
+ defer in.Close()
+
+ inputCh := make(chan []byte, 10000)
+ outputCh := make(chan codeRecord, 10000)
+ wg := sync.WaitGroup{}
+ numWorkers := 2
+ wg.Add(numWorkers)
+ doneCh := make(chan struct{})
+ for i := 0; i < numWorkers; i++ {
+ go func() {
+ for {
+ record, ok := <-inputCh
+ if !ok {
+ break
+ }
+ opName := gno.Op(record[0]).String()
+ if record[1] != 0 {
+ opName = bm.StoreCodeString(record[1])
+ }
+
+ elapsedTime := binary.LittleEndian.Uint32(record[2:])
+ size := binary.LittleEndian.Uint32(record[6:])
+ outputCh <- codeRecord{opName, elapsedTime, size}
+ }
+ wg.Done()
+ }()
+ }
+
+ crs := []codeRecord{}
+ // out put
+ go func() {
+ out, err := os.Create(*outFlag)
+ if err != nil {
+ panic("could not create readable output file: " + err.Error())
+ }
+ defer out.Close()
+ fmt.Fprintln(out, "op,elapsedTime,diskIOBytes")
+
+ for {
+ output, ok := <-outputCh
+ if !ok {
+ break
+ }
+ csv := output.codeName + "," + fmt.Sprint(output.elapsed) + "," + fmt.Sprint(output.size)
+ fmt.Fprintln(out, csv)
+ crs = append(crs, output)
+ }
+
+ out.Close()
+ doneCh <- struct{}{}
+ }()
+
+ recordSize := bm.RecordSize
+ bufSize := recordSize * 100000
+ buf := make([]byte, bufSize)
+
+ for {
+ nbytes, err := in.Read(buf)
+
+ if err != nil && nbytes == 0 {
+ break
+ }
+ n := nbytes / recordSize
+
+ for j := 0; j < n; j++ {
+ inputCh <- buf[j*recordSize : (j+1)*recordSize]
+ }
+ }
+
+ close(inputCh)
+ wg.Wait()
+ close(outputCh)
+ <-doneCh
+ close(doneCh)
+
+ calculateStats(crs)
+ fmt.Println("done")
+}
+
+func calculateStats(crs []codeRecord) {
+ filename := *outFlag
+ out, err := os.Create(addSuffix(filename))
+ if err != nil {
+ panic("could not create readable output file: " + err.Error())
+ }
+ defer out.Close()
+ fmt.Fprintln(out, "op,avg_time,avg_size,time_stddev,count")
+
+ m := make(map[string][]codeRecord)
+ for _, v := range crs {
+ crs, ok := m[v.codeName]
+ if ok {
+ crs = append(crs, v)
+ m[v.codeName] = crs
+ } else {
+ m[v.codeName] = []codeRecord{v}
+ }
+ }
+
+ keys := make([]string, 0, 100)
+
+ for k := range m {
+ keys = append(keys, k)
+ }
+ sort.Slice(keys, func(i, j int) bool {
+ return keys[i] < keys[j]
+ })
+
+ for _, k := range keys {
+ cs := calculate(k, m[k])
+ csv := cs.codeName + "," + fmt.Sprint(cs.avgTime) + "," + fmt.Sprint(cs.avgSize) + "," + fmt.Sprint(cs.timeStdDev) + "," + fmt.Sprint(cs.count)
+ fmt.Fprintln(out, csv)
+ }
+
+ fmt.Println("## Benchmark results saved in:", filename)
+ fmt.Println("## Benchmark result stats saved in:", out.Name())
+}
+
+func addSuffix(filename string) string {
+ // Find the position of the last dot
+ dotPos := strings.LastIndex(filename, ".")
+ if dotPos == -1 {
+ // No dot found, return the original filename with '_status' appended
+ return filename + "_stats"
+ }
+ // Insert '_status' before the last suffix
+ return filename[:dotPos] + "_stats" + filename[dotPos:]
+}
+
+// calcuate the average and standard deviation in time of a code name
+
+func calculate(codeName string, crs []codeRecord) codeStats {
+ // Calculate average
+ var sumTime int64
+ var sumSize int64
+ for _, cr := range crs {
+ t := cr.elapsed
+ s := cr.size
+ sumTime += int64(t)
+ sumSize += int64(s)
+ }
+ avgTime := float64(sumTime) / float64(len(crs))
+ avgSize := float64(sumSize) / float64(len(crs))
+
+ // Calculate standard deviation of duration in time
+ var varianceSum float64
+ for _, cr := range crs {
+ varianceSum += math.Pow(float64(cr.elapsed)-avgTime, 2)
+ }
+ variance := varianceSum / float64(len(crs))
+ stdDev := math.Sqrt(variance)
+ return codeStats{codeName, int64(avgTime), int64(avgSize), int64(stdDev), len(crs)}
+}
diff --git a/gnovm/cmd/benchops/storage_test.go b/gnovm/cmd/benchops/storage_test.go
new file mode 100644
index 00000000000..4883235e1b4
--- /dev/null
+++ b/gnovm/cmd/benchops/storage_test.go
@@ -0,0 +1,39 @@
+package main
+
+import (
+ "path/filepath"
+ "strings"
+ "testing"
+
+ gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestBenchStoreSet(t *testing.T) {
+ assert := assert.New(t)
+
+ dir := "../../pkg/benchops/gno"
+ bstore := benchmarkDiskStore()
+ t.Cleanup(func() { bstore.Delete() })
+ gstore := bstore.gnoStore
+
+ // load stdlibs
+ loadStdlibs(bstore)
+ avlPkgDir := filepath.Join(dir, "avl")
+ addPackage(gstore, avlPkgDir, "gno.land/p/demo/avl")
+
+ storagePkgDir := filepath.Join(dir, "storage")
+ pv := addPackage(gstore, storagePkgDir, storagePkgPath)
+ benchStoreSet(bstore, pv)
+ // verify the post content from all three boards
+ for i := 0; i < 3; i++ {
+ for j := 0; j < rounds; j++ {
+ cx := gno.Call("GetPost", gno.X(0), gno.X(0))
+ res := callFunc(gstore, pv, cx)
+ parts := strings.Split(res[0].V.String(), ",")
+ p := strings.Trim(parts[1], `\"`)
+ expected := strings.Repeat("a", 1024)
+ assert.Equal(p, expected, "it should be 1 KB of character a")
+ }
+ }
+}
diff --git a/gnovm/cmd/benchops/store.go b/gnovm/cmd/benchops/store.go
new file mode 100644
index 00000000000..0020a835d43
--- /dev/null
+++ b/gnovm/cmd/benchops/store.go
@@ -0,0 +1,62 @@
+package main
+
+import (
+ "log"
+ "os"
+ "path/filepath"
+
+ gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
+ "github.com/gnolang/gno/tm2/pkg/bft/config"
+ dbm "github.com/gnolang/gno/tm2/pkg/db"
+ _ "github.com/gnolang/gno/tm2/pkg/db/goleveldb"
+ "github.com/gnolang/gno/tm2/pkg/store"
+ "github.com/gnolang/gno/tm2/pkg/store/dbadapter"
+ "github.com/gnolang/gno/tm2/pkg/store/iavl"
+)
+
+const maxAllocTx = 500 * 1000 * 1000
+
+type BenchStore struct {
+ mulStore store.MultiStore
+ gnoStore gno.Store
+ dir string
+}
+
+func (bStore BenchStore) Write() {
+ bStore.mulStore.MultiWrite()
+}
+
+func (bStore BenchStore) Delete() error {
+ return os.RemoveAll(bStore.dir)
+}
+
+func benchmarkDiskStore() BenchStore {
+ storeDir, err := os.MkdirTemp("", "gno-bench-store-")
+ if err != nil {
+ log.Fatal("unable to get absolute path for storage directory.", err)
+ }
+
+ db, err := dbm.NewDB("gnolang", dbm.GoLevelDBBackend, filepath.Join(storeDir, config.DefaultDBDir))
+ if err != nil {
+ log.Fatalf("error initializing database %q using path %q: %s\n", dbm.GoLevelDBBackend, storeDir, err)
+ }
+
+ baseKey := store.NewStoreKey("baseKey")
+ iavlKey := store.NewStoreKey("iavlKey")
+ ms := store.NewCommitMultiStore(db)
+ ms.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, db)
+ ms.MountStoreWithDB(iavlKey, iavl.StoreConstructor, db)
+ ms.LoadLatestVersion()
+ msCache := ms.MultiCacheWrap()
+
+ alloc := gno.NewAllocator(maxAllocTx)
+ baseSDKStore := msCache.GetStore(baseKey)
+ iavlSDKStore := msCache.GetStore(iavlKey)
+ gStore := gno.NewStore(alloc, baseSDKStore, iavlSDKStore)
+
+ return BenchStore{
+ mulStore: msCache,
+ gnoStore: gStore,
+ dir: storeDir,
+ }
+}
diff --git a/gnovm/cmd/gno/clean.go b/gnovm/cmd/gno/clean.go
index 19a73c51794..0ca2e940d58 100644
--- a/gnovm/cmd/gno/clean.go
+++ b/gnovm/cmd/gno/clean.go
@@ -9,7 +9,6 @@ import (
"path/filepath"
"strings"
- "github.com/gnolang/gno/gnovm/pkg/gnoenv"
"github.com/gnolang/gno/gnovm/pkg/gnomod"
"github.com/gnolang/gno/tm2/pkg/commands"
)
@@ -55,7 +54,7 @@ func (c *cleanCfg) RegisterFlags(fs *flag.FlagSet) {
&c.modCache,
"modcache",
false,
- "remove the entire module download cache",
+ "remove the entire module download cache and exit",
)
}
@@ -64,6 +63,19 @@ func execClean(cfg *cleanCfg, args []string, io commands.IO) error {
return flag.ErrHelp
}
+ if cfg.modCache {
+ modCacheDir := gnomod.ModCachePath()
+ if !cfg.dryRun {
+ if err := os.RemoveAll(modCacheDir); err != nil {
+ return err
+ }
+ }
+ if cfg.dryRun || cfg.verbose {
+ io.Println("rm -rf", modCacheDir)
+ }
+ return nil
+ }
+
path, err := os.Getwd()
if err != nil {
return err
@@ -81,17 +93,6 @@ func execClean(cfg *cleanCfg, args []string, io commands.IO) error {
return err
}
- if cfg.modCache {
- modCacheDir := filepath.Join(gnoenv.HomeDir(), "pkg", "mod")
- if !cfg.dryRun {
- if err := os.RemoveAll(modCacheDir); err != nil {
- return err
- }
- }
- if cfg.dryRun || cfg.verbose {
- io.Println("rm -rf", modCacheDir)
- }
- }
return nil
}
diff --git a/gnovm/cmd/gno/clean_test.go b/gnovm/cmd/gno/clean_test.go
index cfca2655031..401d0c87ddc 100644
--- a/gnovm/cmd/gno/clean_test.go
+++ b/gnovm/cmd/gno/clean_test.go
@@ -32,6 +32,17 @@ func TestCleanApp(t *testing.T) {
testDir: "../../tests/integ/minimalist_gnomod",
simulateExternalRepo: true,
},
+ {
+ args: []string{"clean", "-modcache"},
+ testDir: "../../tests/integ/empty_dir",
+ simulateExternalRepo: true,
+ },
+ {
+ args: []string{"clean", "-modcache", "-n"},
+ testDir: "../../tests/integ/empty_dir",
+ simulateExternalRepo: true,
+ stdoutShouldContain: "rm -rf ",
+ },
}
testMainCaseRun(t, tc)
diff --git a/gnovm/cmd/gno/download_deps.go b/gnovm/cmd/gno/download_deps.go
new file mode 100644
index 00000000000..4e638eb4970
--- /dev/null
+++ b/gnovm/cmd/gno/download_deps.go
@@ -0,0 +1,87 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload"
+ "github.com/gnolang/gno/gnovm/pkg/gnolang"
+ "github.com/gnolang/gno/gnovm/pkg/gnomod"
+ "github.com/gnolang/gno/gnovm/pkg/packages"
+ "github.com/gnolang/gno/tm2/pkg/commands"
+ "golang.org/x/mod/module"
+)
+
+// downloadDeps recursively fetches the imports of a local package while following a given gno.mod replace directives
+func downloadDeps(io commands.IO, pkgDir string, gnoMod *gnomod.File, fetcher pkgdownload.PackageFetcher) error {
+ if fetcher == nil {
+ return errors.New("fetcher is nil")
+ }
+
+ pkg, err := gnolang.ReadMemPackage(pkgDir, gnoMod.Module.Mod.Path)
+ if err != nil {
+ return fmt.Errorf("read package at %q: %w", pkgDir, err)
+ }
+ importsMap, err := packages.Imports(pkg, nil)
+ if err != nil {
+ return fmt.Errorf("read imports at %q: %w", pkgDir, err)
+ }
+ imports := importsMap.Merge(packages.FileKindPackageSource, packages.FileKindTest, packages.FileKindXTest)
+
+ for _, pkgPath := range imports {
+ resolved := gnoMod.Resolve(module.Version{Path: pkgPath.PkgPath})
+ resolvedPkgPath := resolved.Path
+
+ if !isRemotePkgPath(resolvedPkgPath) {
+ continue
+ }
+
+ depDir := gnomod.PackageDir("", module.Version{Path: resolvedPkgPath})
+
+ if err := downloadPackage(io, resolvedPkgPath, depDir, fetcher); err != nil {
+ return fmt.Errorf("download import %q of %q: %w", resolvedPkgPath, pkgDir, err)
+ }
+
+ if err := downloadDeps(io, depDir, gnoMod, fetcher); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// downloadPackage downloads a remote gno package by pkg path and store it at dst
+func downloadPackage(io commands.IO, pkgPath string, dst string, fetcher pkgdownload.PackageFetcher) error {
+ modFilePath := filepath.Join(dst, "gno.mod")
+
+ if _, err := os.Stat(modFilePath); err == nil {
+ // modfile exists in modcache, do nothing
+ return nil
+ } else if !os.IsNotExist(err) {
+ return fmt.Errorf("stat downloaded module %q at %q: %w", pkgPath, dst, err)
+ }
+
+ io.ErrPrintfln("gno: downloading %s", pkgPath)
+
+ if err := pkgdownload.Download(pkgPath, dst, fetcher); err != nil {
+ return err
+ }
+
+ // We need to write a marker file for each downloaded package.
+ // For example: if you first download gno.land/r/foo/bar then download gno.land/r/foo,
+ // we need to know that gno.land/r/foo is not downloaded yet.
+ // We do this by checking for the presence of gno.land/r/foo/gno.mod
+ if err := os.WriteFile(modFilePath, []byte("module "+pkgPath+"\n"), 0o644); err != nil {
+ return fmt.Errorf("write modfile at %q: %w", modFilePath, err)
+ }
+
+ return nil
+}
+
+// isRemotePkgPath determines whether s is a remote pkg path, i.e.: not a filepath nor a standard library
+func isRemotePkgPath(s string) bool {
+ return !strings.HasPrefix(s, ".") && !filepath.IsAbs(s) && !gnolang.IsStdlib(s)
+}
diff --git a/gnovm/cmd/gno/download_deps_test.go b/gnovm/cmd/gno/download_deps_test.go
new file mode 100644
index 00000000000..0828e9b2245
--- /dev/null
+++ b/gnovm/cmd/gno/download_deps_test.go
@@ -0,0 +1,152 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher"
+ "github.com/gnolang/gno/gnovm/pkg/gnomod"
+ "github.com/gnolang/gno/tm2/pkg/commands"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/mod/modfile"
+ "golang.org/x/mod/module"
+)
+
+func TestDownloadDeps(t *testing.T) {
+ for _, tc := range []struct {
+ desc string
+ pkgPath string
+ modFile gnomod.File
+ errorShouldContain string
+ requirements []string
+ ioErrContains []string
+ }{
+ {
+ desc: "not_exists",
+ pkgPath: "gno.land/p/demo/does_not_exists",
+ modFile: gnomod.File{
+ Module: &modfile.Module{
+ Mod: module.Version{
+ Path: "testFetchDeps",
+ },
+ },
+ },
+ errorShouldContain: "query files list for pkg \"gno.land/p/demo/does_not_exists\": package \"gno.land/p/demo/does_not_exists\" is not available",
+ }, {
+ desc: "fetch_gno.land/p/demo/avl",
+ pkgPath: "gno.land/p/demo/avl",
+ modFile: gnomod.File{
+ Module: &modfile.Module{
+ Mod: module.Version{
+ Path: "testFetchDeps",
+ },
+ },
+ },
+ requirements: []string{"avl"},
+ ioErrContains: []string{
+ "gno: downloading gno.land/p/demo/avl",
+ },
+ }, {
+ desc: "fetch_gno.land/p/demo/blog6",
+ pkgPath: "gno.land/p/demo/blog",
+ modFile: gnomod.File{
+ Module: &modfile.Module{
+ Mod: module.Version{
+ Path: "testFetchDeps",
+ },
+ },
+ },
+ requirements: []string{"avl", "blog", "diff", "uassert", "ufmt", "mux"},
+ ioErrContains: []string{
+ "gno: downloading gno.land/p/demo/blog",
+ "gno: downloading gno.land/p/demo/avl",
+ "gno: downloading gno.land/p/demo/ufmt",
+ },
+ }, {
+ desc: "fetch_replace_gno.land/p/demo/avl",
+ pkgPath: "gno.land/p/demo/replaced_avl",
+ modFile: gnomod.File{
+ Module: &modfile.Module{
+ Mod: module.Version{
+ Path: "testFetchDeps",
+ },
+ },
+ Replace: []*modfile.Replace{{
+ Old: module.Version{Path: "gno.land/p/demo/replaced_avl"},
+ New: module.Version{Path: "gno.land/p/demo/avl"},
+ }},
+ },
+ requirements: []string{"avl"},
+ ioErrContains: []string{
+ "gno: downloading gno.land/p/demo/avl",
+ },
+ }, {
+ desc: "fetch_replace_local",
+ pkgPath: "gno.land/p/demo/foo",
+ modFile: gnomod.File{
+ Module: &modfile.Module{
+ Mod: module.Version{
+ Path: "testFetchDeps",
+ },
+ },
+ Replace: []*modfile.Replace{{
+ Old: module.Version{Path: "gno.land/p/demo/foo"},
+ New: module.Version{Path: "../local_foo"},
+ }},
+ },
+ },
+ } {
+ t.Run(tc.desc, func(t *testing.T) {
+ mockErr := bytes.NewBufferString("")
+ io := commands.NewTestIO()
+ io.SetErr(commands.WriteNopCloser(mockErr))
+
+ dirPath := t.TempDir()
+
+ err := os.WriteFile(filepath.Join(dirPath, "main.gno"), []byte(fmt.Sprintf("package main\n\n import %q\n", tc.pkgPath)), 0o644)
+ require.NoError(t, err)
+
+ tmpGnoHome := t.TempDir()
+ t.Setenv("GNOHOME", tmpGnoHome)
+
+ fetcher := examplespkgfetcher.New()
+
+ // gno: downloading dependencies
+ err = downloadDeps(io, dirPath, &tc.modFile, fetcher)
+ if tc.errorShouldContain != "" {
+ require.ErrorContains(t, err, tc.errorShouldContain)
+ } else {
+ require.Nil(t, err)
+
+ // Read dir
+ entries, err := os.ReadDir(filepath.Join(tmpGnoHome, "pkg", "mod", "gno.land", "p", "demo"))
+ if !os.IsNotExist(err) {
+ require.Nil(t, err)
+ }
+
+ // Check dir entries
+ assert.Equal(t, len(tc.requirements), len(entries))
+ for _, e := range entries {
+ assert.Contains(t, tc.requirements, e.Name())
+ }
+
+ // Check logs
+ for _, c := range tc.ioErrContains {
+ assert.Contains(t, mockErr.String(), c)
+ }
+
+ mockErr.Reset()
+
+ // Try fetching again. Should be cached
+ downloadDeps(io, dirPath, &tc.modFile, fetcher)
+ for _, c := range tc.ioErrContains {
+ assert.NotContains(t, mockErr.String(), c)
+ }
+ }
+ })
+ }
+}
diff --git a/gnovm/cmd/gno/env_test.go b/gnovm/cmd/gno/env_test.go
index 8aeb84ab2cc..b15658ed4f5 100644
--- a/gnovm/cmd/gno/env_test.go
+++ b/gnovm/cmd/gno/env_test.go
@@ -18,13 +18,13 @@ func TestEnvApp(t *testing.T) {
{args: []string{"env", "foo"}, stdoutShouldBe: "\n"},
{args: []string{"env", "foo", "bar"}, stdoutShouldBe: "\n\n"},
{args: []string{"env", "GNOROOT"}, stdoutShouldBe: testGnoRootEnv + "\n"},
- {args: []string{"env", "GNOHOME", "storm"}, stdoutShouldBe: testGnoHomeEnv + "\n\n"},
+ {args: []string{"env", "GNOHOME", "storm"}, stdoutShouldBe: testGnoHomeEnv + "\n\n", noTmpGnohome: true},
{args: []string{"env"}, stdoutShouldContain: fmt.Sprintf("GNOROOT=%q", testGnoRootEnv)},
- {args: []string{"env"}, stdoutShouldContain: fmt.Sprintf("GNOHOME=%q", testGnoHomeEnv)},
+ {args: []string{"env"}, stdoutShouldContain: fmt.Sprintf("GNOHOME=%q", testGnoHomeEnv), noTmpGnohome: true},
// json
{args: []string{"env", "-json"}, stdoutShouldContain: fmt.Sprintf("\"GNOROOT\": %q", testGnoRootEnv)},
- {args: []string{"env", "-json"}, stdoutShouldContain: fmt.Sprintf("\"GNOHOME\": %q", testGnoHomeEnv)},
+ {args: []string{"env", "-json"}, stdoutShouldContain: fmt.Sprintf("\"GNOHOME\": %q", testGnoHomeEnv), noTmpGnohome: true},
{
args: []string{"env", "-json", "GNOROOT"},
stdoutShouldBe: fmt.Sprintf("{\n\t\"GNOROOT\": %q\n}\n", testGnoRootEnv),
diff --git a/gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher/examplespkgfetcher.go b/gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher/examplespkgfetcher.go
new file mode 100644
index 00000000000..1642c62d21e
--- /dev/null
+++ b/gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher/examplespkgfetcher.go
@@ -0,0 +1,52 @@
+// Package examplespkgfetcher provides an implementation of [pkgdownload.PackageFetcher]
+// to fetch packages from the examples folder at GNOROOT
+package examplespkgfetcher
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/gnolang/gno/gnovm"
+ "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload"
+ "github.com/gnolang/gno/gnovm/pkg/gnoenv"
+)
+
+type ExamplesPackageFetcher struct{}
+
+var _ pkgdownload.PackageFetcher = (*ExamplesPackageFetcher)(nil)
+
+func New() pkgdownload.PackageFetcher {
+ return &ExamplesPackageFetcher{}
+}
+
+// FetchPackage implements [pkgdownload.PackageFetcher].
+func (e *ExamplesPackageFetcher) FetchPackage(pkgPath string) ([]*gnovm.MemFile, error) {
+ pkgDir := filepath.Join(gnoenv.RootDir(), "examples", filepath.FromSlash(pkgPath))
+
+ entries, err := os.ReadDir(pkgDir)
+ if os.IsNotExist(err) {
+ return nil, fmt.Errorf("query files list for pkg %q: package %q is not available", pkgPath, pkgPath)
+ } else if err != nil {
+ return nil, err
+ }
+
+ res := []*gnovm.MemFile{}
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+
+ name := entry.Name()
+ filePath := filepath.Join(pkgDir, name)
+
+ body, err := os.ReadFile(filePath)
+ if err != nil {
+ return nil, fmt.Errorf("read file at %q: %w", filePath, err)
+ }
+
+ res = append(res, &gnovm.MemFile{Name: name, Body: string(body)})
+ }
+
+ return res, nil
+}
diff --git a/gnovm/cmd/gno/internal/pkgdownload/pkgdownload.go b/gnovm/cmd/gno/internal/pkgdownload/pkgdownload.go
new file mode 100644
index 00000000000..722cab01555
--- /dev/null
+++ b/gnovm/cmd/gno/internal/pkgdownload/pkgdownload.go
@@ -0,0 +1,30 @@
+// Package pkgdownload provides interfaces and utility functions to download gno packages files.
+package pkgdownload
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+)
+
+// Download downloads the package identified by `pkgPath` in the directory at `dst` using the provided [PackageFetcher].
+// The directory at `dst` is created if it does not exists.
+func Download(pkgPath string, dst string, fetcher PackageFetcher) error {
+ files, err := fetcher.FetchPackage(pkgPath)
+ if err != nil {
+ return err
+ }
+
+ if err := os.MkdirAll(dst, 0o744); err != nil {
+ return err
+ }
+
+ for _, file := range files {
+ fileDst := filepath.Join(dst, file.Name)
+ if err := os.WriteFile(fileDst, []byte(file.Body), 0o644); err != nil {
+ return fmt.Errorf("write file at %q: %w", fileDst, err)
+ }
+ }
+
+ return nil
+}
diff --git a/gnovm/cmd/gno/internal/pkgdownload/pkgfetcher.go b/gnovm/cmd/gno/internal/pkgdownload/pkgfetcher.go
new file mode 100644
index 00000000000..79a7a6a54e2
--- /dev/null
+++ b/gnovm/cmd/gno/internal/pkgdownload/pkgfetcher.go
@@ -0,0 +1,7 @@
+package pkgdownload
+
+import "github.com/gnolang/gno/gnovm"
+
+type PackageFetcher interface {
+ FetchPackage(pkgPath string) ([]*gnovm.MemFile, error)
+}
diff --git a/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher.go b/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher.go
new file mode 100644
index 00000000000..a71c1d43719
--- /dev/null
+++ b/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher.go
@@ -0,0 +1,89 @@
+// Package rpcpkgfetcher provides an implementation of [pkgdownload.PackageFetcher]
+// to fetch packages from gno.land rpc endpoints
+package rpcpkgfetcher
+
+import (
+ "fmt"
+ "path"
+ "strings"
+
+ "github.com/gnolang/gno/gnovm"
+ "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload"
+ "github.com/gnolang/gno/tm2/pkg/bft/rpc/client"
+)
+
+type gnoPackageFetcher struct {
+ remoteOverrides map[string]string
+}
+
+var _ pkgdownload.PackageFetcher = (*gnoPackageFetcher)(nil)
+
+func New(remoteOverrides map[string]string) pkgdownload.PackageFetcher {
+ return &gnoPackageFetcher{
+ remoteOverrides: remoteOverrides,
+ }
+}
+
+// FetchPackage implements [pkgdownload.PackageFetcher].
+func (gpf *gnoPackageFetcher) FetchPackage(pkgPath string) ([]*gnovm.MemFile, error) {
+ rpcURL, err := rpcURLFromPkgPath(pkgPath, gpf.remoteOverrides)
+ if err != nil {
+ return nil, fmt.Errorf("get rpc url for pkg path %q: %w", pkgPath, err)
+ }
+
+ client, err := client.NewHTTPClient(rpcURL)
+ if err != nil {
+ return nil, fmt.Errorf("failed to instantiate tm2 client with remote %q: %w", rpcURL, err)
+ }
+ defer client.Close()
+
+ data, err := qfile(client, pkgPath)
+ if err != nil {
+ return nil, fmt.Errorf("query files list for pkg %q: %w", pkgPath, err)
+ }
+
+ files := strings.Split(string(data), "\n")
+ res := make([]*gnovm.MemFile, len(files))
+ for i, file := range files {
+ filePath := path.Join(pkgPath, file)
+ data, err := qfile(client, filePath)
+ if err != nil {
+ return nil, fmt.Errorf("query package file %q: %w", filePath, err)
+ }
+
+ res[i] = &gnovm.MemFile{Name: file, Body: string(data)}
+ }
+ return res, nil
+}
+
+func rpcURLFromPkgPath(pkgPath string, remoteOverrides map[string]string) (string, error) {
+ parts := strings.Split(pkgPath, "/")
+ if len(parts) < 2 {
+ return "", fmt.Errorf("bad pkg path %q", pkgPath)
+ }
+ domain := parts[0]
+
+ if override, ok := remoteOverrides[domain]; ok {
+ return override, nil
+ }
+
+ // XXX: retrieve host/port from r/sys/zones.
+ rpcURL := fmt.Sprintf("https://rpc.%s:443", domain)
+
+ return rpcURL, nil
+}
+
+func qfile(c client.Client, pkgPath string) ([]byte, error) {
+ path := "vm/qfile"
+ data := []byte(pkgPath)
+
+ qres, err := c.ABCIQuery(path, data)
+ if err != nil {
+ return nil, fmt.Errorf("query qfile: %w", err)
+ }
+ if qres.Response.Error != nil {
+ return nil, fmt.Errorf("qfile failed: %w\n%s", qres.Response.Error, qres.Response.Log)
+ }
+
+ return qres.Response.Data, nil
+}
diff --git a/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher_test.go b/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher_test.go
new file mode 100644
index 00000000000..56db5b796de
--- /dev/null
+++ b/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher_test.go
@@ -0,0 +1,53 @@
+package rpcpkgfetcher
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestRpcURLFromPkgPath(t *testing.T) {
+ cases := []struct {
+ name string
+ pkgPath string
+ overrides map[string]string
+ result string
+ errorContains string
+ }{
+ {
+ name: "happy path simple",
+ pkgPath: "gno.land/p/demo/avl",
+ result: "https://rpc.gno.land:443",
+ },
+ {
+ name: "happy path override",
+ pkgPath: "gno.land/p/demo/avl",
+ overrides: map[string]string{"gno.land": "https://example.com/rpc:42"},
+ result: "https://example.com/rpc:42",
+ },
+ {
+ name: "happy path override no effect",
+ pkgPath: "gno.land/p/demo/avl",
+ overrides: map[string]string{"some.chain": "https://example.com/rpc:42"},
+ result: "https://rpc.gno.land:443",
+ },
+ {
+ name: "error bad pkg path",
+ pkgPath: "std",
+ result: "",
+ errorContains: `bad pkg path "std"`,
+ },
+ }
+
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ res, err := rpcURLFromPkgPath(c.pkgPath, c.overrides)
+ if len(c.errorContains) == 0 {
+ require.NoError(t, err)
+ } else {
+ require.ErrorContains(t, err, c.errorContains)
+ }
+ require.Equal(t, c.result, res)
+ })
+ }
+}
diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go
index 6c497c7e2c0..ce3465b484e 100644
--- a/gnovm/cmd/gno/lint.go
+++ b/gnovm/cmd/gno/lint.go
@@ -6,17 +6,20 @@ import (
"flag"
"fmt"
"go/scanner"
- "io"
+ "go/types"
+ goio "io"
"os"
"path/filepath"
"regexp"
"strings"
+ "github.com/gnolang/gno/gnovm"
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
- "github.com/gnolang/gno/gnovm/tests"
+ "github.com/gnolang/gno/gnovm/pkg/gnomod"
+ "github.com/gnolang/gno/gnovm/pkg/test"
"github.com/gnolang/gno/tm2/pkg/commands"
- osm "github.com/gnolang/gno/tm2/pkg/os"
+ "go.uber.org/multierr"
)
type lintCfg struct {
@@ -49,6 +52,31 @@ func (c *lintCfg) RegisterFlags(fs *flag.FlagSet) {
fs.StringVar(&c.rootDir, "root-dir", rootdir, "clone location of github.com/gnolang/gno (gno tries to guess it)")
}
+type lintCode int
+
+const (
+ lintUnknown lintCode = iota
+ lintGnoMod
+ lintGnoError
+ lintParserError
+ lintTypeCheckError
+
+ // TODO: add new linter codes here.
+)
+
+type lintIssue struct {
+ Code lintCode
+ Msg string
+ Confidence float64 // 1 is 100%
+ Location string // file:line, or equivalent
+ // TODO: consider writing fix suggestions
+}
+
+func (i lintIssue) String() string {
+ // TODO: consider crafting a doc URL based on Code.
+ return fmt.Sprintf("%s: %s (code=%d)", i.Location, i.Msg, i.Code)
+}
+
func execLint(cfg *lintCfg, args []string, io commands.IO) error {
if len(args) < 1 {
return flag.ErrHelp
@@ -69,68 +97,82 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error {
hasError := false
+ bs, ts := test.Store(
+ rootDir, false,
+ nopReader{}, goio.Discard, goio.Discard,
+ )
+
for _, pkgPath := range pkgPaths {
if verbose {
- fmt.Fprintf(io.Err(), "Linting %q...\n", pkgPath)
+ io.ErrPrintln(pkgPath)
+ }
+
+ info, err := os.Stat(pkgPath)
+ if err == nil && !info.IsDir() {
+ pkgPath = filepath.Dir(pkgPath)
}
// Check if 'gno.mod' exists
- gnoModPath := filepath.Join(pkgPath, "gno.mod")
- if !osm.FileExists(gnoModPath) {
- hasError = true
+ gmFile, err := gnomod.ParseAt(pkgPath)
+ if err != nil {
issue := lintIssue{
- Code: lintNoGnoMod,
+ Code: lintGnoMod,
Confidence: 1,
Location: pkgPath,
- Msg: "missing 'gno.mod' file",
+ Msg: err.Error(),
}
- fmt.Fprint(io.Err(), issue.String()+"\n")
+ io.ErrPrintln(issue)
+ hasError = true
+ }
+
+ memPkg, err := gno.ReadMemPackage(pkgPath, pkgPath)
+ if err != nil {
+ io.ErrPrintln(issueFromError(pkgPath, err).String())
+ hasError = true
+ continue
+ }
+
+ // Perform imports using the parent store.
+ if err := test.LoadImports(ts, memPkg); err != nil {
+ io.ErrPrintln(issueFromError(pkgPath, err).String())
+ hasError = true
+ continue
}
// Handle runtime errors
- hasError = catchRuntimeError(pkgPath, io.Err(), func() {
- stdout, stdin, stderr := io.Out(), io.In(), io.Err()
- testStore := tests.TestStore(
- rootDir, "",
- stdin, stdout, stderr,
- tests.ImportModeStdlibsOnly,
- )
-
- targetPath := pkgPath
- info, err := os.Stat(pkgPath)
- if err == nil && !info.IsDir() {
- targetPath = filepath.Dir(pkgPath)
+ hasRuntimeErr := catchRuntimeError(pkgPath, io.Err(), func() {
+ // Wrap in cache wrap so execution of the linter doesn't impact
+ // other packages.
+ cw := bs.CacheWrap()
+ gs := ts.BeginTransaction(cw, cw, nil)
+
+ // Run type checking
+ if gmFile == nil || !gmFile.Draft {
+ foundErr, err := lintTypeCheck(io, memPkg, gs)
+ if err != nil {
+ io.ErrPrintln(err)
+ hasError = true
+ } else if foundErr {
+ hasError = true
+ }
+ } else if verbose {
+ io.ErrPrintfln("%s: module is draft, skipping type check", pkgPath)
}
- memPkg := gno.ReadMemPackage(targetPath, targetPath)
- tm := tests.TestMachine(testStore, stdout, memPkg.Name)
+ tm := test.Machine(gs, goio.Discard, memPkg.Path)
+ defer tm.Release()
// Check package
tm.RunMemPackage(memPkg, true)
// Check test files
- testfiles := &gno.FileSet{}
- for _, mfile := range memPkg.Files {
- if !strings.HasSuffix(mfile.Name, ".gno") {
- continue // Skip non-GNO files
- }
-
- n, _ := gno.ParseFile(mfile.Name, mfile.Body)
- if n == nil {
- continue // Skip empty files
- }
-
- // XXX: package ending with `_test` is not supported yet
- if strings.HasSuffix(mfile.Name, "_test.gno") && !strings.HasSuffix(string(n.PkgName), "_test") {
- // Keep only test files
- testfiles.AddFiles(n)
- }
- }
+ testFiles := lintTestFiles(memPkg)
- tm.RunFiles(testfiles.Files...)
- }) || hasError
-
- // TODO: Add more checkers
+ tm.RunFiles(testFiles.Files...)
+ })
+ if hasRuntimeErr {
+ hasError = true
+ }
}
if hasError {
@@ -140,6 +182,66 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error {
return nil
}
+func lintTypeCheck(io commands.IO, memPkg *gnovm.MemPackage, testStore gno.Store) (errorsFound bool, err error) {
+ tcErr := gno.TypeCheckMemPackageTest(memPkg, testStore)
+ if tcErr == nil {
+ return false, nil
+ }
+
+ errs := multierr.Errors(tcErr)
+ for _, err := range errs {
+ switch err := err.(type) {
+ case types.Error:
+ io.ErrPrintln(lintIssue{
+ Code: lintTypeCheckError,
+ Msg: err.Msg,
+ Confidence: 1,
+ Location: err.Fset.Position(err.Pos).String(),
+ })
+ case scanner.ErrorList:
+ for _, scErr := range err {
+ io.ErrPrintln(lintIssue{
+ Code: lintParserError,
+ Msg: scErr.Msg,
+ Confidence: 1,
+ Location: scErr.Pos.String(),
+ })
+ }
+ case scanner.Error:
+ io.ErrPrintln(lintIssue{
+ Code: lintParserError,
+ Msg: err.Msg,
+ Confidence: 1,
+ Location: err.Pos.String(),
+ })
+ default:
+ return false, fmt.Errorf("unexpected error type: %T", err)
+ }
+ }
+ return true, nil
+}
+
+func lintTestFiles(memPkg *gnovm.MemPackage) *gno.FileSet {
+ testfiles := &gno.FileSet{}
+ for _, mfile := range memPkg.Files {
+ if !strings.HasSuffix(mfile.Name, ".gno") {
+ continue // Skip non-GNO files
+ }
+
+ n, _ := gno.ParseFile(mfile.Name, mfile.Body)
+ if n == nil {
+ continue // Skip empty files
+ }
+
+ // XXX: package ending with `_test` is not supported yet
+ if strings.HasSuffix(mfile.Name, "_test.gno") && !strings.HasSuffix(string(n.PkgName), "_test") {
+ // Keep only test files
+ testfiles.AddFiles(n)
+ }
+ }
+ return testfiles
+}
+
func guessSourcePath(pkg, source string) string {
if info, err := os.Stat(pkg); !os.IsNotExist(err) && !info.IsDir() {
pkg = filepath.Dir(pkg)
@@ -160,9 +262,9 @@ func guessSourcePath(pkg, source string) string {
// reParseRecover is a regex designed to parse error details from a string.
// It extracts the file location, line number, and error message from a formatted error string.
// XXX: Ideally, error handling should encapsulate location details within a dedicated error type.
-var reParseRecover = regexp.MustCompile(`^([^:]+):(\d+)(?::\d+)?:? *(.*)$`)
+var reParseRecover = regexp.MustCompile(`^([^:]+)((?::(?:\d+)){1,2}):? *(.*)$`)
-func catchRuntimeError(pkgPath string, stderr io.WriteCloser, action func()) (hasError bool) {
+func catchRuntimeError(pkgPath string, stderr goio.WriteCloser, action func()) (hasError bool) {
defer func() {
// Errors catched here mostly come from: gnovm/pkg/gnolang/preprocess.go
r := recover()
@@ -173,15 +275,21 @@ func catchRuntimeError(pkgPath string, stderr io.WriteCloser, action func()) (ha
switch verr := r.(type) {
case *gno.PreprocessError:
err := verr.Unwrap()
- fmt.Fprint(stderr, issueFromError(pkgPath, err).String()+"\n")
- case scanner.ErrorList:
- for _, err := range verr {
- fmt.Fprint(stderr, issueFromError(pkgPath, err).String()+"\n")
- }
+ fmt.Fprintln(stderr, issueFromError(pkgPath, err).String())
case error:
- fmt.Fprint(stderr, issueFromError(pkgPath, verr).String()+"\n")
+ errors := multierr.Errors(verr)
+ for _, err := range errors {
+ errList, ok := err.(scanner.ErrorList)
+ if ok {
+ for _, errorInList := range errList {
+ fmt.Fprintln(stderr, issueFromError(pkgPath, errorInList).String())
+ }
+ } else {
+ fmt.Fprintln(stderr, issueFromError(pkgPath, err).String())
+ }
+ }
case string:
- fmt.Fprint(stderr, issueFromError(pkgPath, errors.New(verr)).String()+"\n")
+ fmt.Fprintln(stderr, issueFromError(pkgPath, errors.New(verr)).String())
default:
panic(r)
}
@@ -191,29 +299,6 @@ func catchRuntimeError(pkgPath string, stderr io.WriteCloser, action func()) (ha
return
}
-type lintCode int
-
-const (
- lintUnknown lintCode = 0
- lintNoGnoMod lintCode = iota
- lintGnoError
-
- // TODO: add new linter codes here.
-)
-
-type lintIssue struct {
- Code lintCode
- Msg string
- Confidence float64 // 1 is 100%
- Location string // file:line, or equivalent
- // TODO: consider writing fix suggestions
-}
-
-func (i lintIssue) String() string {
- // TODO: consider crafting a doc URL based on Code.
- return fmt.Sprintf("%s: %s (code=%d).", i.Location, i.Msg, i.Code)
-}
-
func issueFromError(pkgPath string, err error) lintIssue {
var issue lintIssue
issue.Confidence = 1
@@ -223,9 +308,9 @@ func issueFromError(pkgPath string, err error) lintIssue {
parsedError = strings.TrimPrefix(parsedError, pkgPath+"/")
matches := reParseRecover.FindStringSubmatch(parsedError)
- if len(matches) == 4 {
+ if len(matches) > 0 {
sourcepath := guessSourcePath(pkgPath, matches[1])
- issue.Location = fmt.Sprintf("%s:%s", sourcepath, matches[2])
+ issue.Location = sourcepath + matches[2]
issue.Msg = strings.TrimSpace(matches[3])
} else {
issue.Location = fmt.Sprintf("%s:0", filepath.Clean(pkgPath))
@@ -233,3 +318,7 @@ func issueFromError(pkgPath string, err error) lintIssue {
}
return issue
}
+
+type nopReader struct{}
+
+func (nopReader) Read(p []byte) (int, error) { return 0, goio.EOF }
diff --git a/gnovm/cmd/gno/lint_test.go b/gnovm/cmd/gno/lint_test.go
index a5c0319cd00..4589fc55f92 100644
--- a/gnovm/cmd/gno/lint_test.go
+++ b/gnovm/cmd/gno/lint_test.go
@@ -1,6 +1,9 @@
package main
-import "testing"
+import (
+ "strings"
+ "testing"
+)
func TestLintApp(t *testing.T) {
tc := []testMainCase{
@@ -9,37 +12,51 @@ func TestLintApp(t *testing.T) {
errShouldBe: "flag: help requested",
}, {
args: []string{"lint", "../../tests/integ/run_main/"},
- stderrShouldContain: "./../../tests/integ/run_main: missing 'gno.mod' file (code=1).",
+ stderrShouldContain: "./../../tests/integ/run_main: gno.mod file not found in current or any parent directory (code=1)",
errShouldBe: "exit code: 1",
}, {
args: []string{"lint", "../../tests/integ/undefined_variable_test/undefined_variables_test.gno"},
- stderrShouldContain: "undefined_variables_test.gno:6: name toto not declared (code=2)",
+ stderrShouldContain: "undefined_variables_test.gno:6:28: name toto not declared (code=2)",
errShouldBe: "exit code: 1",
}, {
args: []string{"lint", "../../tests/integ/package_not_declared/main.gno"},
- stderrShouldContain: "main.gno:4: name fmt not declared (code=2).",
+ stderrShouldContain: "main.gno:4:2: name fmt not declared (code=2)",
errShouldBe: "exit code: 1",
}, {
args: []string{"lint", "../../tests/integ/several-lint-errors/main.gno"},
- stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5: expected ';', found example (code=2).\n../../tests/integ/several-lint-errors/main.gno:6",
+ stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5:5: expected ';', found example (code=2)\n../../tests/integ/several-lint-errors/main.gno:6",
errShouldBe: "exit code: 1",
}, {
- args: []string{"lint", "../../tests/integ/run_main/"},
- stderrShouldContain: "./../../tests/integ/run_main: missing 'gno.mod' file (code=1).",
- errShouldBe: "exit code: 1",
+ args: []string{"lint", "../../tests/integ/several-files-multiple-errors/main.gno"},
+ stderrShouldContain: func() string {
+ lines := []string{
+ "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2)",
+ "../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2)",
+ "../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2)",
+ "../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2)",
+ }
+ return strings.Join(lines, "\n") + "\n"
+ }(),
+ errShouldBe: "exit code: 1",
}, {
args: []string{"lint", "../../tests/integ/minimalist_gnomod/"},
// TODO: raise an error because there is a gno.mod, but no .gno files
}, {
args: []string{"lint", "../../tests/integ/invalid_module_name/"},
// TODO: raise an error because gno.mod is invalid
+ }, {
+ args: []string{"lint", "../../tests/integ/invalid_gno_file/"},
+ stderrShouldContain: "../../tests/integ/invalid_gno_file/invalid.gno:1:1: expected 'package', found packag (code=2)",
+ errShouldBe: "exit code: 1",
+ }, {
+ args: []string{"lint", "../../tests/integ/typecheck_missing_return/"},
+ stderrShouldContain: "../../tests/integ/typecheck_missing_return/main.gno:5:1: missing return (code=4)",
+ errShouldBe: "exit code: 1",
},
// TODO: 'gno mod' is valid?
- // TODO: is gno source valid?
// TODO: are dependencies valid?
// TODO: is gno source using unsafe/discouraged features?
- // TODO: consider making `gno transpile; go lint *gen.go`
// TODO: check for imports of native libs from non _test.gno files
}
testMainCaseRun(t, tc)
diff --git a/gnovm/cmd/gno/main_test.go b/gnovm/cmd/gno/main_test.go
index 1797d0aede9..2ea3e31f977 100644
--- a/gnovm/cmd/gno/main_test.go
+++ b/gnovm/cmd/gno/main_test.go
@@ -9,9 +9,9 @@ import (
"strings"
"testing"
- "github.com/stretchr/testify/require"
-
+ "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher"
"github.com/gnolang/gno/tm2/pkg/commands"
+ "github.com/stretchr/testify/require"
)
func TestMain_Gno(t *testing.T) {
@@ -26,6 +26,7 @@ type testMainCase struct {
args []string
testDir string
simulateExternalRepo bool
+ noTmpGnohome bool
// for the following FooContain+FooBe expected couples, if both are empty,
// then the test suite will require that the "got" is not empty.
@@ -58,6 +59,10 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) {
mockOut := bytes.NewBufferString("")
mockErr := bytes.NewBufferString("")
+ if !test.noTmpGnohome {
+ t.Setenv("GNOHOME", t.TempDir())
+ }
+
checkOutputs := func(t *testing.T) {
t.Helper()
@@ -123,12 +128,14 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) {
io.SetOut(commands.WriteNopCloser(mockOut))
io.SetErr(commands.WriteNopCloser(mockErr))
+ testPackageFetcher = examplespkgfetcher.New()
+
err := newGnocliCmd(io).ParseAndRun(context.Background(), test.args)
if errShouldBeEmpty {
require.Nil(t, err, "err should be nil")
} else {
- t.Log("err", err.Error())
+ t.Log("err", fmt.Sprintf("%v", err))
require.NotNil(t, err, "err shouldn't be nil")
if test.errShouldContain != "" {
require.Contains(t, err.Error(), test.errShouldContain, "err should contain")
diff --git a/gnovm/cmd/gno/mod.go b/gnovm/cmd/gno/mod.go
index fec1b0ab2c1..5479d934ce6 100644
--- a/gnovm/cmd/gno/mod.go
+++ b/gnovm/cmd/gno/mod.go
@@ -4,19 +4,22 @@ import (
"context"
"flag"
"fmt"
- "go/parser"
- "go/token"
"os"
"path/filepath"
- "sort"
"strings"
+ "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload"
+ "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher"
"github.com/gnolang/gno/gnovm/pkg/gnomod"
+ "github.com/gnolang/gno/gnovm/pkg/packages"
"github.com/gnolang/gno/tm2/pkg/commands"
"github.com/gnolang/gno/tm2/pkg/errors"
"go.uber.org/multierr"
)
+// testPackageFetcher allows to override the package fetcher during tests.
+var testPackageFetcher pkgdownload.PackageFetcher
+
func newModCmd(io commands.IO) *commands.Command {
cmd := commands.NewCommand(
commands.Metadata{
@@ -123,23 +126,17 @@ For example:
}
type modDownloadCfg struct {
- remote string
- verbose bool
+ remoteOverrides string
}
+const remoteOverridesArgName = "remote-overrides"
+
func (c *modDownloadCfg) RegisterFlags(fs *flag.FlagSet) {
fs.StringVar(
- &c.remote,
- "remote",
- "test3.gno.land:26657",
- "remote for fetching gno modules",
- )
-
- fs.BoolVar(
- &c.verbose,
- "v",
- false,
- "verbose output when running",
+ &c.remoteOverrides,
+ remoteOverridesArgName,
+ "",
+ "chain-domain=rpc-url comma-separated list",
)
}
@@ -148,6 +145,17 @@ func execModDownload(cfg *modDownloadCfg, args []string, io commands.IO) error {
return flag.ErrHelp
}
+ fetcher := testPackageFetcher
+ if fetcher == nil {
+ remoteOverrides, err := parseRemoteOverrides(cfg.remoteOverrides)
+ if err != nil {
+ return fmt.Errorf("invalid %s flag: %w", remoteOverridesArgName, err)
+ }
+ fetcher = rpcpkgfetcher.New(remoteOverrides)
+ } else if len(cfg.remoteOverrides) != 0 {
+ return fmt.Errorf("can't use %s flag with a custom package fetcher", remoteOverridesArgName)
+ }
+
path, err := os.Getwd()
if err != nil {
return err
@@ -176,23 +184,26 @@ func execModDownload(cfg *modDownloadCfg, args []string, io commands.IO) error {
return fmt.Errorf("validate: %w", err)
}
- // fetch dependencies
- if err := gnoMod.FetchDeps(gnomod.GetGnoModPath(), cfg.remote, cfg.verbose); err != nil {
- return fmt.Errorf("fetch: %w", err)
+ if err := downloadDeps(io, path, gnoMod, fetcher); err != nil {
+ return err
}
- gomod, err := gnomod.GnoToGoMod(*gnoMod)
- if err != nil {
- return fmt.Errorf("sanitize: %w", err)
- }
+ return nil
+}
- // write go.mod file
- err = gomod.Write(filepath.Join(path, "go.mod"))
- if err != nil {
- return fmt.Errorf("write go.mod file: %w", err)
+func parseRemoteOverrides(arg string) (map[string]string, error) {
+ pairs := strings.Split(arg, ",")
+ res := make(map[string]string, len(pairs))
+ for _, pair := range pairs {
+ parts := strings.Split(pair, "=")
+ if len(parts) != 2 {
+ return nil, fmt.Errorf("expected 2 parts in chain-domain=rpc-url pair %q", arg)
+ }
+ domain := strings.TrimSpace(parts[0])
+ rpcURL := strings.TrimSpace(parts[1])
+ res[domain] = rpcURL
}
-
- return nil
+ return res, nil
}
func execModInit(args []string) error {
@@ -276,26 +287,6 @@ func modTidyOnce(cfg *modTidyCfg, wd, pkgdir string, io commands.IO) error {
return err
}
- // Drop all existing requires
- for _, r := range gm.Require {
- gm.DropRequire(r.Mod.Path)
- }
-
- imports, err := getGnoPackageImports(pkgdir)
- if err != nil {
- return err
- }
- for _, im := range imports {
- // skip if importpath is modulepath
- if im == gm.Module.Mod.Path {
- continue
- }
- gm.AddRequire(im, "v0.0.0-latest")
- if cfg.verbose {
- io.ErrPrintfln(" %s", im)
- }
- }
-
gm.Write(fname)
return nil
}
@@ -366,79 +357,19 @@ func getImportToFilesMap(pkgPath string) (map[string][]string, error) {
if strings.HasSuffix(filename, "_filetest.gno") {
continue
}
- imports, err := getGnoFileImports(filepath.Join(pkgPath, filename))
+
+ data, err := os.ReadFile(filepath.Join(pkgPath, filename))
if err != nil {
return nil, err
}
-
- for _, imp := range imports {
- m[imp] = append(m[imp], filename)
- }
- }
- return m, nil
-}
-
-// getGnoPackageImports returns the list of gno imports from a given path.
-// Note: It ignores subdirs. Since right now we are still deciding on
-// how to handle subdirs.
-// See:
-// - https://github.com/gnolang/gno/issues/1024
-// - https://github.com/gnolang/gno/issues/852
-//
-// TODO: move this to better location.
-func getGnoPackageImports(path string) ([]string, error) {
- entries, err := os.ReadDir(path)
- if err != nil {
- return nil, err
- }
-
- allImports := make([]string, 0)
- seen := make(map[string]struct{})
- for _, e := range entries {
- filename := e.Name()
- if ext := filepath.Ext(filename); ext != ".gno" {
- continue
- }
- if strings.HasSuffix(filename, "_filetest.gno") {
- continue
- }
- imports, err := getGnoFileImports(filepath.Join(path, filename))
+ imports, err := packages.FileImports(filename, string(data), nil)
if err != nil {
return nil, err
}
- for _, im := range imports {
- if !strings.HasPrefix(im, "gno.land/") {
- continue
- }
- if _, ok := seen[im]; ok {
- continue
- }
- allImports = append(allImports, im)
- seen[im] = struct{}{}
- }
- }
- sort.Strings(allImports)
- return allImports, nil
-}
-
-func getGnoFileImports(fname string) ([]string, error) {
- if !strings.HasSuffix(fname, ".gno") {
- return nil, fmt.Errorf("not a gno file: %q", fname)
- }
- data, err := os.ReadFile(fname)
- if err != nil {
- return nil, err
- }
- fs := token.NewFileSet()
- f, err := parser.ParseFile(fs, fname, data, parser.ImportsOnly)
- if err != nil {
- return nil, err
- }
- res := make([]string, 0)
- for _, im := range f.Imports {
- importPath := strings.TrimPrefix(strings.TrimSuffix(im.Path.Value, `"`), `"`)
- res = append(res, importPath)
+ for _, imp := range imports {
+ m[imp.PkgPath] = append(m[imp.PkgPath], filename)
+ }
}
- return res, nil
+ return m, nil
}
diff --git a/gnovm/cmd/gno/mod_test.go b/gnovm/cmd/gno/mod_test.go
index d35ab311b6c..afce25597cd 100644
--- a/gnovm/cmd/gno/mod_test.go
+++ b/gnovm/cmd/gno/mod_test.go
@@ -1,12 +1,7 @@
package main
import (
- "os"
- "path/filepath"
"testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
)
func TestModApp(t *testing.T) {
@@ -44,24 +39,19 @@ func TestModApp(t *testing.T) {
args: []string{"mod", "download"},
testDir: "../../tests/integ/require_remote_module",
simulateExternalRepo: true,
+ stderrShouldContain: "gno: downloading gno.land/p/demo/avl",
},
{
args: []string{"mod", "download"},
testDir: "../../tests/integ/require_invalid_module",
simulateExternalRepo: true,
- errShouldContain: "fetch: writepackage: querychain",
+ stderrShouldContain: "gno: downloading gno.land/p/demo/notexists",
+ errShouldContain: "query files list for pkg \"gno.land/p/demo/notexists\": package \"gno.land/p/demo/notexists\" is not available",
},
{
args: []string{"mod", "download"},
- testDir: "../../tests/integ/invalid_module_version1",
+ testDir: "../../tests/integ/require_std_lib",
simulateExternalRepo: true,
- errShouldContain: "usage: require module/path v1.2.3",
- },
- {
- args: []string{"mod", "download"},
- testDir: "../../tests/integ/invalid_module_version2",
- simulateExternalRepo: true,
- errShouldContain: "invalid: must be of the form v1.2.3",
},
{
args: []string{"mod", "download"},
@@ -72,12 +62,14 @@ func TestModApp(t *testing.T) {
args: []string{"mod", "download"},
testDir: "../../tests/integ/replace_with_module",
simulateExternalRepo: true,
+ stderrShouldContain: "gno: downloading gno.land/p/demo/users",
},
{
args: []string{"mod", "download"},
testDir: "../../tests/integ/replace_with_invalid_module",
simulateExternalRepo: true,
- errShouldContain: "fetch: writepackage: querychain",
+ stderrShouldContain: "gno: downloading gno.land/p/demo/notexists",
+ errShouldContain: "query files list for pkg \"gno.land/p/demo/notexists\": package \"gno.land/p/demo/notexists\" is not available",
},
// test `gno mod init` with no module name
@@ -158,12 +150,6 @@ func TestModApp(t *testing.T) {
simulateExternalRepo: true,
errShouldContain: "could not read gno.mod file",
},
- {
- args: []string{"mod", "tidy"},
- testDir: "../../tests/integ/invalid_module_version1",
- simulateExternalRepo: true,
- errShouldContain: "error parsing gno.mod file at",
- },
{
args: []string{"mod", "tidy"},
testDir: "../../tests/integ/minimalist_gnomod",
@@ -179,12 +165,6 @@ func TestModApp(t *testing.T) {
testDir: "../../tests/integ/valid2",
simulateExternalRepo: true,
},
- {
- args: []string{"mod", "tidy"},
- testDir: "../../tests/integ/invalid_gno_file",
- simulateExternalRepo: true,
- errShouldContain: "expected 'package', found packag",
- },
// test `gno mod why`
{
@@ -199,12 +179,6 @@ func TestModApp(t *testing.T) {
simulateExternalRepo: true,
errShouldContain: "could not read gno.mod file",
},
- {
- args: []string{"mod", "why", "std"},
- testDir: "../../tests/integ/invalid_module_version1",
- simulateExternalRepo: true,
- errShouldContain: "error parsing gno.mod file at",
- },
{
args: []string{"mod", "why", "std"},
testDir: "../../tests/integ/invalid_gno_file",
@@ -239,122 +213,6 @@ valid.gno
`,
},
}
- testMainCaseRun(t, tc)
-}
-
-func TestGetGnoImports(t *testing.T) {
- workingDir, err := os.Getwd()
- require.NoError(t, err)
-
- // create external dir
- tmpDir, cleanUpFn := createTmpDir(t)
- defer cleanUpFn()
-
- // cd to tmp directory
- os.Chdir(tmpDir)
- defer os.Chdir(workingDir)
-
- files := []struct {
- name, data string
- }{
- {
- name: "file1.gno",
- data: `
- package tmp
-
- import (
- "std"
-
- "gno.land/p/demo/pkg1"
- )
- `,
- },
- {
- name: "file2.gno",
- data: `
- package tmp
-
- import (
- "gno.land/p/demo/pkg1"
- "gno.land/p/demo/pkg2"
- )
- `,
- },
- {
- name: "file1_test.gno",
- data: `
- package tmp
-
- import (
- "testing"
-
- "gno.land/p/demo/testpkg"
- )
- `,
- },
- {
- name: "z_0_filetest.gno",
- data: `
- package main
-
- import (
- "gno.land/p/demo/filetestpkg"
- )
- `,
- },
-
- // subpkg files
- {
- name: filepath.Join("subtmp", "file1.gno"),
- data: `
- package subtmp
-
- import (
- "std"
-
- "gno.land/p/demo/subpkg1"
- )
- `,
- },
- {
- name: filepath.Join("subtmp", "file2.gno"),
- data: `
- package subtmp
-
- import (
- "gno.land/p/demo/subpkg1"
- "gno.land/p/demo/subpkg2"
- )
- `,
- },
- }
-
- // Expected list of imports
- // - ignore subdirs
- // - ignore duplicate
- // - ignore *_filetest.gno
- // - should be sorted
- expected := []string{
- "gno.land/p/demo/pkg1",
- "gno.land/p/demo/pkg2",
- "gno.land/p/demo/testpkg",
- }
-
- // Create subpkg dir
- err = os.Mkdir("subtmp", 0o700)
- require.NoError(t, err)
-
- // Create files
- for _, f := range files {
- err = os.WriteFile(f.name, []byte(f.data), 0o644)
- require.NoError(t, err)
- }
- imports, err := getGnoPackageImports(tmpDir)
- require.NoError(t, err)
-
- require.Equal(t, len(expected), len(imports))
- for i := range imports {
- assert.Equal(t, expected[i], imports[i])
- }
+ testMainCaseRun(t, tc)
}
diff --git a/gnovm/cmd/gno/run.go b/gnovm/cmd/gno/run.go
index cfbfe995a46..9a9beac5cd1 100644
--- a/gnovm/cmd/gno/run.go
+++ b/gnovm/cmd/gno/run.go
@@ -12,8 +12,9 @@ import (
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
- "github.com/gnolang/gno/gnovm/tests"
+ "github.com/gnolang/gno/gnovm/pkg/test"
"github.com/gnolang/gno/tm2/pkg/commands"
+ "github.com/gnolang/gno/tm2/pkg/std"
)
type runCfg struct {
@@ -91,9 +92,9 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error {
stderr := io.Err()
// init store and machine
- testStore := tests.TestStore(cfg.rootDir,
- "", stdin, stdout, stderr,
- tests.ImportModeStdlibsPreferred)
+ _, testStore := test.Store(
+ cfg.rootDir, false,
+ stdin, stdout, stderr)
if cfg.verbose {
testStore.SetLogStoreOps(true)
}
@@ -112,11 +113,15 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error {
return errors.New("no files to run")
}
+ var send std.Coins
+ pkgPath := string(files[0].PkgName)
+ ctx := test.Context(pkgPath, send)
m := gno.NewMachineWithOptions(gno.MachineOptions{
- PkgPath: string(files[0].PkgName),
- Input: stdin,
+ PkgPath: pkgPath,
Output: stdout,
+ Input: stdin,
Store: testStore,
+ Context: ctx,
Debug: cfg.debug || cfg.debugAddr != "",
})
diff --git a/gnovm/cmd/gno/run_test.go b/gnovm/cmd/gno/run_test.go
index 79a873cdfe5..aa7780c149e 100644
--- a/gnovm/cmd/gno/run_test.go
+++ b/gnovm/cmd/gno/run_test.go
@@ -1,6 +1,9 @@
package main
-import "testing"
+import (
+ "strings"
+ "testing"
+)
func TestRunApp(t *testing.T) {
tc := []testMainCase{
@@ -79,6 +82,23 @@ func TestRunApp(t *testing.T) {
args: []string{"run", "../../tests/integ/invalid_assign/main.gno"},
recoverShouldContain: "cannot use bool as main.C without explicit conversion",
},
+ {
+ args: []string{"run", "-expr", "Context()", "../../tests/integ/context/context.gno"},
+ stdoutShouldContain: "Context worked",
+ },
+ {
+ args: []string{"run", "../../tests/integ/several-files-multiple-errors/"},
+ stderrShouldContain: func() string {
+ lines := []string{
+ "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2)",
+ "../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2)",
+ "../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2)",
+ "../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2)",
+ }
+ return strings.Join(lines, "\n") + "\n"
+ }(),
+ errShouldBe: "exit code: 1",
+ },
// TODO: a test file
// TODO: args
// TODO: nativeLibs VS stdlibs
diff --git a/gnovm/cmd/gno/test.go b/gnovm/cmd/gno/test.go
index 5884463a552..ea06b25d8e2 100644
--- a/gnovm/cmd/gno/test.go
+++ b/gnovm/cmd/gno/test.go
@@ -1,31 +1,21 @@
package main
import (
- "bytes"
"context"
- "encoding/json"
"flag"
"fmt"
+ goio "io"
"log"
- "os"
"path/filepath"
- "runtime/debug"
- "sort"
"strings"
- "text/template"
"time"
- "go.uber.org/multierr"
-
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
"github.com/gnolang/gno/gnovm/pkg/gnomod"
- "github.com/gnolang/gno/gnovm/tests"
+ "github.com/gnolang/gno/gnovm/pkg/test"
"github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/errors"
"github.com/gnolang/gno/tm2/pkg/random"
- "github.com/gnolang/gno/tm2/pkg/std"
- "github.com/gnolang/gno/tm2/pkg/testutils"
)
type testCfg struct {
@@ -35,7 +25,7 @@ type testCfg struct {
timeout time.Duration
updateGoldenTests bool
printRuntimeMetrics bool
- withNativeFallback bool
+ printEvents bool
}
func newTestCmd(io commands.IO) *commands.Command {
@@ -63,34 +53,38 @@ module name found in 'gno.mod', or else it is randomly generated like
- "*_filetest.gno" files on the other hand are kind of unique. They exist to
provide a way to interact and assert a gno contract, thanks to a set of
-specific instructions that can be added using code comments.
+specific directives that can be added using code comments.
"*_filetest.gno" must be declared in the 'main' package and so must have a
'main' function, that will be executed to test the target contract.
-List of available instructions that can be used in "*_filetest.gno" files:
- - "PKGPATH:" is a single line instruction that can be used to define the
+These single-line directives can set "input parameters" for the machine used
+to perform the test:
+ - "PKGPATH:" is a single line directive that can be used to define the
package used to interact with the tested package. If not specified, "main" is
used.
- - "MAXALLOC:" is a signle line instruction that can be used to define a limit
+ - "MAXALLOC:" is a single line directive that can be used to define a limit
to the VM allocator. If this limit is exceeded, the VM will panic. Default to
0, no limit.
- - "SEND:" is a single line instruction that can be used to send an amount of
+ - "SEND:" is a single line directive that can be used to send an amount of
token along with the transaction. The format is for example "1000000ugnot".
Default is empty.
- - "Output:\n" (*) is a multiple lines instruction that can be used to assert
- the output of the "*_filetest.gno" file. Any prints executed inside the
- 'main' function must match the lines that follows the "Output:\n"
- instruction, or else the test fails.
- - "Error:\n" works similarly to "Output:\n", except that it asserts the
- stderr of the program, which in that case, comes from the VM because of a
- panic, rather than the 'main' function.
- - "Realm:\n" (*) is a multiple lines instruction that can be used to assert
- what has been recorded in the store following the execution of the 'main'
- function.
-
-(*) The 'update-golden-tests' flag can be set to fill out the content of the
-instruction with the actual content of the test instead of failing.
+
+These directives, instead, match the comment that follows with the result
+of the GnoVM, acting as a "golden test":
+ - "Output:" tests the following comment with the standard output of the
+ filetest.
+ - "Error:" tests the following comment with any panic, or other kind of
+ error that the filetest generates (like a parsing or preprocessing error).
+ - "Realm:" tests the following comment against the store log, which can show
+ what realm information is stored.
+ - "Stacktrace:" can be used to verify the following lines against the
+ stacktrace of the error.
+ - "Events:" can be used to verify the emitted events against a JSON.
+
+To speed up execution, imports of pure packages are processed separately from
+the execution of the tests. This makes testing faster, but means that the
+initialization of imported pure packages cannot be checked in filetests.
`,
},
cfg,
@@ -112,7 +106,7 @@ func (c *testCfg) RegisterFlags(fs *flag.FlagSet) {
&c.updateGoldenTests,
"update-golden-tests",
false,
- `writes actual as wanted for "Output:" and "Realm:" instructions`,
+ `writes actual as wanted for "golden" directives in filetests`,
)
fs.StringVar(
@@ -137,23 +131,24 @@ func (c *testCfg) RegisterFlags(fs *flag.FlagSet) {
)
fs.BoolVar(
- &c.withNativeFallback,
- "with-native-fallback",
+ &c.printRuntimeMetrics,
+ "print-runtime-metrics",
false,
- "use stdlibs/* if present, otherwise use supported native Go packages",
+ "print runtime metrics (gas, memory, cpu cycles)",
)
fs.BoolVar(
- &c.printRuntimeMetrics,
- "print-runtime-metrics",
+ &c.printEvents,
+ "print-events",
false,
- "print runtime metrics (gas, memory, cpu cycles)",
+ "print emitted events",
)
}
func execTest(cfg *testCfg, args []string, io commands.IO) error {
- if len(args) < 1 {
- return flag.ErrHelp
+ // Default to current directory if no args provided
+ if len(args) == 0 {
+ args = []string{"."}
}
// guess opts.RootDir
@@ -165,6 +160,7 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error {
if err != nil {
return fmt.Errorf("list targets from patterns: %w", err)
}
+
if len(paths) == 0 {
io.ErrPrintln("no packages to test")
return nil
@@ -182,6 +178,18 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error {
return fmt.Errorf("list sub packages: %w", err)
}
+ // Set up options to run tests.
+ stdout := goio.Discard
+ if cfg.verbose {
+ stdout = io.Out()
+ }
+ opts := test.NewTestOptions(cfg.rootDir, io.In(), stdout, io.Err())
+ opts.RunFlag = cfg.run
+ opts.Sync = cfg.updateGoldenTests
+ opts.Verbose = cfg.verbose
+ opts.Metrics = cfg.printRuntimeMetrics
+ opts.Events = cfg.printEvents
+
buildErrCount := 0
testErrCount := 0
for _, pkg := range subPkgs {
@@ -189,199 +197,48 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error {
io.ErrPrintfln("? %s \t[no test files]", pkg.Dir)
continue
}
-
- sort.Strings(pkg.TestGnoFiles)
- sort.Strings(pkg.FiletestGnoFiles)
-
- startedAt := time.Now()
- err = gnoTestPkg(pkg.Dir, pkg.TestGnoFiles, pkg.FiletestGnoFiles, cfg, io)
- duration := time.Since(startedAt)
- dstr := fmtDuration(duration)
-
- if err != nil {
- io.ErrPrintfln("%s: test pkg: %v", pkg.Dir, err)
- io.ErrPrintfln("FAIL")
- io.ErrPrintfln("FAIL %s \t%s", pkg.Dir, dstr)
- io.ErrPrintfln("FAIL")
- testErrCount++
- } else {
- io.ErrPrintfln("ok %s \t%s", pkg.Dir, dstr)
- }
- }
- if testErrCount > 0 || buildErrCount > 0 {
- io.ErrPrintfln("FAIL")
- return fmt.Errorf("FAIL: %d build errors, %d test errors", buildErrCount, testErrCount)
- }
-
- return nil
-}
-
-func gnoTestPkg(
- pkgPath string,
- unittestFiles,
- filetestFiles []string,
- cfg *testCfg,
- io commands.IO,
-) error {
- var (
- verbose = cfg.verbose
- rootDir = cfg.rootDir
- runFlag = cfg.run
- printRuntimeMetrics = cfg.printRuntimeMetrics
-
- stdin = io.In()
- stdout = io.Out()
- stderr = io.Err()
- errs error
- )
-
- mode := tests.ImportModeStdlibsOnly
- if cfg.withNativeFallback {
- // XXX: display a warn?
- mode = tests.ImportModeStdlibsPreferred
- }
- if !verbose {
- // TODO: speedup by ignoring if filter is file/*?
- mockOut := bytes.NewBufferString("")
- stdout = commands.WriteNopCloser(mockOut)
- }
-
- // testing with *_test.gno
- if len(unittestFiles) > 0 {
// Determine gnoPkgPath by reading gno.mod
var gnoPkgPath string
- modfile, err := gnomod.ParseAt(pkgPath)
+ modfile, err := gnomod.ParseAt(pkg.Dir)
if err == nil {
gnoPkgPath = modfile.Module.Mod.Path
} else {
- gnoPkgPath = pkgPathFromRootDir(pkgPath, rootDir)
+ gnoPkgPath = pkgPathFromRootDir(pkg.Dir, cfg.rootDir)
if gnoPkgPath == "" {
// unable to read pkgPath from gno.mod, generate a random realm path
io.ErrPrintfln("--- WARNING: unable to read package path from gno.mod or gno root directory; try creating a gno.mod file")
- gnoPkgPath = gno.RealmPathPrefix + random.RandStr(8)
+ gnoPkgPath = "gno.land/r/" + strings.ToLower(random.RandStr(8)) // XXX: gno.land hardcoded for convenience.
}
}
- memPkg := gno.ReadMemPackage(pkgPath, gnoPkgPath)
- // tfiles, ifiles := gno.ParseMemPackageTests(memPkg)
- var tfiles, ifiles *gno.FileSet
+ memPkg := gno.MustReadMemPackage(pkg.Dir, gnoPkgPath)
- hasError := catchRuntimeError(gnoPkgPath, stderr, func() {
- tfiles, ifiles = parseMemPackageTests(memPkg)
+ startedAt := time.Now()
+ hasError := catchRuntimeError(gnoPkgPath, io.Err(), func() {
+ err = test.Test(memPkg, pkg.Dir, opts)
})
- if hasError {
- return commands.ExitCodeError(1)
- }
- testPkgName := getPkgNameFromFileset(ifiles)
-
- // run test files in pkg
- if len(tfiles.Files) > 0 {
- testStore := tests.TestStore(
- rootDir, "",
- stdin, stdout, stderr,
- mode,
- )
- if verbose {
- testStore.SetLogStoreOps(true)
- }
-
- m := tests.TestMachine(testStore, stdout, gnoPkgPath)
- if printRuntimeMetrics {
- // from tm2/pkg/sdk/vm/keeper.go
- // XXX: make maxAllocTx configurable.
- maxAllocTx := int64(500 * 1000 * 1000)
-
- m.Alloc = gno.NewAllocator(maxAllocTx)
- }
- m.RunMemPackage(memPkg, true)
- err := runTestFiles(m, tfiles, memPkg.Name, verbose, printRuntimeMetrics, runFlag, io)
- if err != nil {
- errs = multierr.Append(errs, err)
- }
- }
-
- // test xxx_test pkg
- if len(ifiles.Files) > 0 {
- testStore := tests.TestStore(
- rootDir, "",
- stdin, stdout, stderr,
- mode,
- )
- if verbose {
- testStore.SetLogStoreOps(true)
- }
-
- m := tests.TestMachine(testStore, stdout, testPkgName)
-
- memFiles := make([]*std.MemFile, 0, len(ifiles.FileNames())+1)
- for _, f := range memPkg.Files {
- for _, ifileName := range ifiles.FileNames() {
- if f.Name == "gno.mod" || f.Name == ifileName {
- memFiles = append(memFiles, f)
- break
- }
- }
- }
-
- memPkg.Files = memFiles
- memPkg.Name = testPkgName
- memPkg.Path = memPkg.Path + "_test"
- m.RunMemPackage(memPkg, true)
+ duration := time.Since(startedAt)
+ dstr := fmtDuration(duration)
- err := runTestFiles(m, ifiles, testPkgName, verbose, printRuntimeMetrics, runFlag, io)
+ if hasError || err != nil {
if err != nil {
- errs = multierr.Append(errs, err)
+ io.ErrPrintfln("%s: test pkg: %v", pkg.Dir, err)
}
+ io.ErrPrintfln("FAIL")
+ io.ErrPrintfln("FAIL %s \t%s", pkg.Dir, dstr)
+ io.ErrPrintfln("FAIL")
+ testErrCount++
+ } else {
+ io.ErrPrintfln("ok %s \t%s", pkg.Dir, dstr)
}
}
-
- // testing with *_filetest.gno
- {
- filter := splitRegexp(runFlag)
- for _, testFile := range filetestFiles {
- testFileName := filepath.Base(testFile)
- testName := "file/" + testFileName
- if !shouldRun(filter, testName) {
- continue
- }
-
- startedAt := time.Now()
- if verbose {
- io.ErrPrintfln("=== RUN %s", testName)
- }
-
- var closer func() (string, error)
- if !verbose {
- closer = testutils.CaptureStdoutAndStderr()
- }
-
- testFilePath := filepath.Join(pkgPath, testFileName)
- err := tests.RunFileTest(rootDir, testFilePath, tests.WithSyncWanted(cfg.updateGoldenTests))
- duration := time.Since(startedAt)
- dstr := fmtDuration(duration)
-
- if err != nil {
- errs = multierr.Append(errs, err)
- io.ErrPrintfln("--- FAIL: %s (%s)", testName, dstr)
- if verbose {
- stdouterr, err := closer()
- if err != nil {
- panic(err)
- }
- fmt.Fprintln(os.Stderr, stdouterr)
- }
- continue
- }
-
- if verbose {
- io.ErrPrintfln("--- PASS: %s (%s)", testName, dstr)
- }
- // XXX: add per-test metrics
- }
+ if testErrCount > 0 || buildErrCount > 0 {
+ io.ErrPrintfln("FAIL")
+ return fmt.Errorf("FAIL: %d build errors, %d test errors", buildErrCount, testErrCount)
}
- return errs
+ return nil
}
// attempts to determine the full gno pkg path by analyzing the directory.
@@ -412,208 +269,3 @@ func pkgPathFromRootDir(pkgPath, rootDir string) string {
}
return ""
}
-
-func runTestFiles(
- m *gno.Machine,
- files *gno.FileSet,
- pkgName string,
- verbose bool,
- printRuntimeMetrics bool,
- runFlag string,
- io commands.IO,
-) (errs error) {
- defer func() {
- if r := recover(); r != nil {
- errs = multierr.Append(fmt.Errorf("panic: %v\nstack:\n%v\ngno machine: %v", r, string(debug.Stack()), m.String()), errs)
- }
- }()
-
- testFuncs := &testFuncs{
- PackageName: pkgName,
- Verbose: verbose,
- RunFlag: runFlag,
- }
- loadTestFuncs(pkgName, testFuncs, files)
-
- // before/after statistics
- numPackagesBefore := m.Store.NumMemPackages()
-
- testmain, err := formatTestmain(testFuncs)
- if err != nil {
- log.Fatal(err)
- }
-
- m.RunFiles(files.Files...)
- n := gno.MustParseFile("main_test.gno", testmain)
- m.RunFiles(n)
-
- for _, test := range testFuncs.Tests {
- testFuncStr := fmt.Sprintf("%q", test.Name)
-
- eval := m.Eval(gno.Call("runtest", testFuncStr))
-
- ret := eval[0].GetString()
- if ret == "" {
- err := errors.New("failed to execute unit test: %q", test.Name)
- errs = multierr.Append(errs, err)
- io.ErrPrintfln("--- FAIL: %s [internal gno testing error]", test.Name)
- continue
- }
-
- // TODO: replace with amino or send native type?
- var rep report
- err = json.Unmarshal([]byte(ret), &rep)
- if err != nil {
- errs = multierr.Append(errs, err)
- io.ErrPrintfln("--- FAIL: %s [internal gno testing error]", test.Name)
- continue
- }
-
- if rep.Failed {
- err := errors.New("failed: %q", test.Name)
- errs = multierr.Append(errs, err)
- }
-
- if printRuntimeMetrics {
- imports := m.Store.NumMemPackages() - numPackagesBefore - 1
- // XXX: store changes
- // XXX: max mem consumption
- allocsVal := "n/a"
- if m.Alloc != nil {
- maxAllocs, allocs := m.Alloc.Status()
- allocsVal = fmt.Sprintf("%s(%.2f%%)",
- prettySize(allocs),
- float64(allocs)/float64(maxAllocs)*100,
- )
- }
- io.ErrPrintfln("--- runtime: cycle=%s imports=%d allocs=%s",
- prettySize(m.Cycles),
- imports,
- allocsVal,
- )
- }
- }
-
- return errs
-}
-
-// mirror of stdlibs/testing.Report
-type report struct {
- Failed bool
- Skipped bool
-}
-
-var testmainTmpl = template.Must(template.New("testmain").Parse(`
-package {{ .PackageName }}
-
-import (
- "testing"
-)
-
-var tests = []testing.InternalTest{
-{{range .Tests}}
- {"{{.Name}}", {{.Name}}},
-{{end}}
-}
-
-func runtest(name string) (report string) {
- for _, test := range tests {
- if test.Name == name {
- return testing.RunTest({{printf "%q" .RunFlag}}, {{.Verbose}}, test)
- }
- }
- panic("no such test: " + name)
- return ""
-}
-`))
-
-type testFuncs struct {
- Tests []testFunc
- PackageName string
- Verbose bool
- RunFlag string
-}
-
-type testFunc struct {
- Package string
- Name string
-}
-
-func getPkgNameFromFileset(files *gno.FileSet) string {
- if len(files.Files) <= 0 {
- return ""
- }
- return string(files.Files[0].PkgName)
-}
-
-func formatTestmain(t *testFuncs) (string, error) {
- var buf bytes.Buffer
- if err := testmainTmpl.Execute(&buf, t); err != nil {
- return "", err
- }
- return buf.String(), nil
-}
-
-func loadTestFuncs(pkgName string, t *testFuncs, tfiles *gno.FileSet) *testFuncs {
- for _, tf := range tfiles.Files {
- for _, d := range tf.Decls {
- if fd, ok := d.(*gno.FuncDecl); ok {
- fname := string(fd.Name)
- if strings.HasPrefix(fname, "Test") {
- tf := testFunc{
- Package: pkgName,
- Name: fname,
- }
- t.Tests = append(t.Tests, tf)
- }
- }
- }
- }
- return t
-}
-
-// parseMemPackageTests is copied from gno.ParseMemPackageTests
-// for except to _filetest.gno
-func parseMemPackageTests(memPkg *std.MemPackage) (tset, itset *gno.FileSet) {
- tset = &gno.FileSet{}
- itset = &gno.FileSet{}
- for _, mfile := range memPkg.Files {
- if !strings.HasSuffix(mfile.Name, ".gno") {
- continue // skip this file.
- }
- if strings.HasSuffix(mfile.Name, "_filetest.gno") {
- continue
- }
- n, err := gno.ParseFile(mfile.Name, mfile.Body)
- if err != nil {
- panic(err)
- }
- if n == nil {
- panic("should not happen")
- }
- if strings.HasSuffix(mfile.Name, "_test.gno") {
- // add test file.
- if memPkg.Name+"_test" == string(n.PkgName) {
- itset.AddFiles(n)
- } else {
- tset.AddFiles(n)
- }
- } else if memPkg.Name == string(n.PkgName) {
- // skip package file.
- } else {
- panic(fmt.Sprintf(
- "expected package name [%s] or [%s_test] but got [%s] file [%s]",
- memPkg.Name, memPkg.Name, n.PkgName, mfile))
- }
- }
- return tset, itset
-}
-
-func shouldRun(filter filterMatch, path string) bool {
- if filter == nil {
- return true
- }
- elem := strings.Split(path, "/")
- ok, _ := filter.matches(elem, matchString)
- return ok
-}
diff --git a/gnovm/cmd/gno/testdata/gno_fmt/empty.txtar b/gnovm/cmd/gno/testdata/fmt/empty.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_fmt/empty.txtar
rename to gnovm/cmd/gno/testdata/fmt/empty.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_fmt/import_cleaning.txtar b/gnovm/cmd/gno/testdata/fmt/import_cleaning.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_fmt/import_cleaning.txtar
rename to gnovm/cmd/gno/testdata/fmt/import_cleaning.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_fmt/include.txtar b/gnovm/cmd/gno/testdata/fmt/include.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_fmt/include.txtar
rename to gnovm/cmd/gno/testdata/fmt/include.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_fmt/multi_import.txtar b/gnovm/cmd/gno/testdata/fmt/multi_import.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_fmt/multi_import.txtar
rename to gnovm/cmd/gno/testdata/fmt/multi_import.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_fmt/noimport_format.txtar b/gnovm/cmd/gno/testdata/fmt/noimport_format.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_fmt/noimport_format.txtar
rename to gnovm/cmd/gno/testdata/fmt/noimport_format.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_fmt/parse_error.txtar b/gnovm/cmd/gno/testdata/fmt/parse_error.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_fmt/parse_error.txtar
rename to gnovm/cmd/gno/testdata/fmt/parse_error.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_fmt/shadow_import.txtar b/gnovm/cmd/gno/testdata/fmt/shadow_import.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_fmt/shadow_import.txtar
rename to gnovm/cmd/gno/testdata/fmt/shadow_import.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar b/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar
deleted file mode 100644
index fc4039d38c6..00000000000
--- a/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar
+++ /dev/null
@@ -1,19 +0,0 @@
-# testing gno lint command: bad import error
-
-! gno lint ./bad_file.gno
-
-cmp stdout stdout.golden
-cmp stderr stderr.golden
-
--- bad_file.gno --
-package main
-
-import "python"
-
-func main() {
- fmt.Println("Hello", 42)
-}
-
--- stdout.golden --
--- stderr.golden --
-bad_file.gno:3: unknown import path python (code=2).
diff --git a/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar b/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar
deleted file mode 100644
index 9482eeb1f4f..00000000000
--- a/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar
+++ /dev/null
@@ -1,20 +0,0 @@
-# gno lint: test file error
-
-! gno lint ./i_have_error_test.gno
-
-cmp stdout stdout.golden
-cmp stderr stderr.golden
-
--- i_have_error_test.gno --
-package main
-
-import "fmt"
-
-func TestIHaveSomeError() {
- i := undefined_variable
- fmt.Println("Hello", 42)
-}
-
--- stdout.golden --
--- stderr.golden --
-i_have_error_test.gno:6: name undefined_variable not declared (code=2).
diff --git a/gnovm/cmd/gno/testdata/gno_lint/file_error_txtar b/gnovm/cmd/gno/testdata/gno_lint/file_error_txtar
deleted file mode 100644
index 9482eeb1f4f..00000000000
--- a/gnovm/cmd/gno/testdata/gno_lint/file_error_txtar
+++ /dev/null
@@ -1,20 +0,0 @@
-# gno lint: test file error
-
-! gno lint ./i_have_error_test.gno
-
-cmp stdout stdout.golden
-cmp stderr stderr.golden
-
--- i_have_error_test.gno --
-package main
-
-import "fmt"
-
-func TestIHaveSomeError() {
- i := undefined_variable
- fmt.Println("Hello", 42)
-}
-
--- stdout.golden --
--- stderr.golden --
-i_have_error_test.gno:6: name undefined_variable not declared (code=2).
diff --git a/gnovm/cmd/gno/testdata/gno_lint/no_error.txtar b/gnovm/cmd/gno/testdata/gno_lint/no_error.txtar
deleted file mode 100644
index 95356b1ba2b..00000000000
--- a/gnovm/cmd/gno/testdata/gno_lint/no_error.txtar
+++ /dev/null
@@ -1,18 +0,0 @@
-# testing simple gno lint command with any error
-
-gno lint ./good_file.gno
-
-cmp stdout stdout.golden
-cmp stdout stderr.golden
-
--- good_file.gno --
-package main
-
-import "fmt"
-
-func main() {
- fmt.Println("Hello", 42)
-}
-
--- stdout.golden --
--- stderr.golden --
diff --git a/gnovm/cmd/gno/testdata/gno_lint/no_gnomod.txtar b/gnovm/cmd/gno/testdata/gno_lint/no_gnomod.txtar
deleted file mode 100644
index 52daa6f0e9b..00000000000
--- a/gnovm/cmd/gno/testdata/gno_lint/no_gnomod.txtar
+++ /dev/null
@@ -1,19 +0,0 @@
-# gno lint: no gnomod
-
-! gno lint .
-
-cmp stdout stdout.golden
-cmp stderr stderr.golden
-
--- good_file.gno --
-package main
-
-import "fmt"
-
-func main() {
- fmt.Println("Hello", 42)
-}
-
--- stdout.golden --
--- stderr.golden --
-./.: missing 'gno.mod' file (code=1).
diff --git a/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar b/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar
deleted file mode 100644
index 7bd74a34855..00000000000
--- a/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar
+++ /dev/null
@@ -1,20 +0,0 @@
-# testing gno lint command: not declared error
-
-! gno lint ./bad_file.gno
-
-cmp stdout stdout.golden
-cmp stderr stderr.golden
-
--- bad_file.gno --
-package main
-
-import "fmt"
-
-func main() {
- hello.Foo()
- fmt.Println("Hello", 42)
-}
-
--- stdout.golden --
--- stderr.golden --
-bad_file.gno:6: name hello not declared (code=2).
diff --git a/gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar b/gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar
deleted file mode 100644
index 00737d8dd67..00000000000
--- a/gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar
+++ /dev/null
@@ -1,17 +0,0 @@
-# Test Error instruction incorrect
-
-! gno test -v .
-
-stdout 'Machine\.RunMain\(\) panic: oups'
-stderr '=== RUN file/x_filetest.gno'
-stderr 'panic: fail on x_filetest.gno: got "oups", want: "xxx"'
-
--- x_filetest.gno --
-package main
-
-func main() {
- panic("oups")
-}
-
-// Error:
-// xxx
diff --git a/gnovm/cmd/gno/testdata/gno_test/error_sync.txtar b/gnovm/cmd/gno/testdata/gno_test/error_sync.txtar
deleted file mode 100644
index e2b67cb3333..00000000000
--- a/gnovm/cmd/gno/testdata/gno_test/error_sync.txtar
+++ /dev/null
@@ -1,32 +0,0 @@
-# Test Error instruction updated
-# NOTE: unlike Output and Realm instruction updates, Error update is not driven
-# by the '-update-golden-tests' flag. The Error is only updated when it is
-# empty.
-
-! gno test -v .
-
-stdout 'Machine\.RunMain\(\) panic: oups'
-stderr '=== RUN file/x_filetest.gno'
-
-cmp x_filetest.gno x_filetest.gno.golden
-
--- x_filetest.gno --
-package main
-
-func main() {
- panic("oups")
-}
-
-// Error:
-
--- x_filetest.gno.golden --
-package main
-
-func main() {
- panic("oups")
-}
-
-// Error:
-// oups
-// *** CHECK THE ERR MESSAGES ABOVE, MAKE SURE IT'S WHAT YOU EXPECTED, DELETE THIS LINE AND RUN TEST AGAIN ***
-
diff --git a/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar b/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar
deleted file mode 100644
index 91431e4f7bb..00000000000
--- a/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar
+++ /dev/null
@@ -1,20 +0,0 @@
-# Test with a failing _filetest.gno file
-
-! gno test -v .
-
-stdout 'Machine.RunMain\(\) panic: beep boop'
-stderr '=== RUN file/failing_filetest.gno'
-stderr 'panic: fail on failing_filetest.gno: got unexpected error: beep boop'
-
--- failing.gno --
-package failing
-
--- failing_filetest.gno --
-package main
-
-func main() {
- panic("beep boop")
-}
-
-// Output:
-// blah
diff --git a/gnovm/cmd/gno/testdata/gno_test/no_args.txtar b/gnovm/cmd/gno/testdata/gno_test/no_args.txtar
deleted file mode 100644
index bd9cd4fc965..00000000000
--- a/gnovm/cmd/gno/testdata/gno_test/no_args.txtar
+++ /dev/null
@@ -1,6 +0,0 @@
-# Run gno test without args
-
-! gno test
-
-! stdout .+
-stderr 'USAGE'
diff --git a/gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar b/gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar
deleted file mode 100644
index 009d09623a0..00000000000
--- a/gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar
+++ /dev/null
@@ -1,23 +0,0 @@
-# Test Output instruction incorrect
-
-! gno test -v .
-
-! stdout .+ # stdout should be empty
-stderr '=== RUN file/x_filetest.gno'
-stderr 'panic: fail on x_filetest.gno: diff:'
-stderr '--- Expected'
-stderr '\+\+\+ Actual'
-stderr '@@ -1,2 \+1 @@'
-stderr 'hey'
-stderr '-hru?'
-
--- x_filetest.gno --
-package main
-
-func main() {
- println("hey")
-}
-
-// Output:
-// hey
-// hru?
diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar b/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar
deleted file mode 100644
index 99e6fccd42d..00000000000
--- a/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar
+++ /dev/null
@@ -1,87 +0,0 @@
-# Test Realm instruction correct
-
-gno test -v .
-
-! stdout .+ # stdout should be empty
-stderr '=== RUN file/x_filetest.gno'
-stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)'
-stderr 'ok \. \d\.\d\ds'
-
--- x_filetest.gno --
-// PKGPATH: gno.land/r/x
-package x
-
-var x int
-
-func main() {
- x = 1
-}
-
-// Realm:
-// switchrealm["gno.land/r/x"]
-// u[58cde29876a8d185e30c727361981efb068f4726:2]={
-// "Blank": {},
-// "ObjectInfo": {
-// "ID": "58cde29876a8d185e30c727361981efb068f4726:2",
-// "IsEscaped": true,
-// "ModTime": "3",
-// "RefCount": "2"
-// },
-// "Parent": null,
-// "Source": {
-// "@type": "/gno.RefNode",
-// "BlockNode": null,
-// "Location": {
-// "Column": "0",
-// "File": "",
-// "Line": "0",
-// "PkgPath": "gno.land/r/x"
-// }
-// },
-// "Values": [
-// {
-// "N": "AQAAAAAAAAA=",
-// "T": {
-// "@type": "/gno.PrimitiveType",
-// "value": "32"
-// }
-// },
-// {
-// "T": {
-// "@type": "/gno.FuncType",
-// "Params": [],
-// "Results": []
-// },
-// "V": {
-// "@type": "/gno.FuncValue",
-// "Closure": {
-// "@type": "/gno.RefValue",
-// "Escaped": true,
-// "ObjectID": "58cde29876a8d185e30c727361981efb068f4726:3"
-// },
-// "FileName": "main.gno",
-// "IsMethod": false,
-// "Name": "main",
-// "NativeName": "",
-// "NativePkg": "",
-// "PkgPath": "gno.land/r/x",
-// "Source": {
-// "@type": "/gno.RefNode",
-// "BlockNode": null,
-// "Location": {
-// "Column": "1",
-// "File": "main.gno",
-// "Line": "6",
-// "PkgPath": "gno.land/r/x"
-// }
-// },
-// "Type": {
-// "@type": "/gno.FuncType",
-// "Params": [],
-// "Results": []
-// }
-// }
-// }
-// ]
-// }
-
diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar b/gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar
deleted file mode 100644
index 6dfd6d70bb9..00000000000
--- a/gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar
+++ /dev/null
@@ -1,26 +0,0 @@
-# Test Realm instruction incorrect
-
-! gno test -v .
-
-! stdout .+ # stdout should be empty
-stderr '=== RUN file/x_filetest.gno'
-stderr 'panic: fail on x_filetest.gno: diff:'
-stderr '--- Expected'
-stderr '\+\+\+ Actual'
-stderr '@@ -1 \+1,66 @@'
-stderr '-xxx'
-stderr '\+switchrealm\["gno.land/r/x"\]'
-
--- x_filetest.gno --
-// PKGPATH: gno.land/r/x
-package x
-
-var x int
-
-func main() {
- x = 1
-}
-
-// Realm:
-// xxxx
-
diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar b/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar
deleted file mode 100644
index 3d27ab4fde0..00000000000
--- a/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar
+++ /dev/null
@@ -1,102 +0,0 @@
-# Test Realm instruction updated
-
-gno test -v . -update-golden-tests
-
-! stdout .+ # stdout should be empty
-stderr '=== RUN file/x_filetest.gno'
-stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)'
-stderr 'ok \. \d\.\d\ds'
-
-cmp x_filetest.gno x_filetest.gno.golden
-
--- x_filetest.gno --
-// PKGPATH: gno.land/r/x
-package x
-
-var x int
-
-func main() {
- x = 1
-}
-
-// Realm:
-// xxx
-
--- x_filetest.gno.golden --
-// PKGPATH: gno.land/r/x
-package x
-
-var x int
-
-func main() {
- x = 1
-}
-
-// Realm:
-// switchrealm["gno.land/r/x"]
-// u[58cde29876a8d185e30c727361981efb068f4726:2]={
-// "Blank": {},
-// "ObjectInfo": {
-// "ID": "58cde29876a8d185e30c727361981efb068f4726:2",
-// "IsEscaped": true,
-// "ModTime": "3",
-// "RefCount": "2"
-// },
-// "Parent": null,
-// "Source": {
-// "@type": "/gno.RefNode",
-// "BlockNode": null,
-// "Location": {
-// "Column": "0",
-// "File": "",
-// "Line": "0",
-// "PkgPath": "gno.land/r/x"
-// }
-// },
-// "Values": [
-// {
-// "N": "AQAAAAAAAAA=",
-// "T": {
-// "@type": "/gno.PrimitiveType",
-// "value": "32"
-// }
-// },
-// {
-// "T": {
-// "@type": "/gno.FuncType",
-// "Params": [],
-// "Results": []
-// },
-// "V": {
-// "@type": "/gno.FuncValue",
-// "Closure": {
-// "@type": "/gno.RefValue",
-// "Escaped": true,
-// "ObjectID": "58cde29876a8d185e30c727361981efb068f4726:3"
-// },
-// "FileName": "main.gno",
-// "IsMethod": false,
-// "Name": "main",
-// "NativeName": "",
-// "NativePkg": "",
-// "PkgPath": "gno.land/r/x",
-// "Source": {
-// "@type": "/gno.RefNode",
-// "BlockNode": null,
-// "Location": {
-// "Column": "1",
-// "File": "main.gno",
-// "Line": "6",
-// "PkgPath": "gno.land/r/x"
-// }
-// },
-// "Type": {
-// "@type": "/gno.FuncType",
-// "Params": [],
-// "Results": []
-// }
-// }
-// }
-// ]
-// }
-
diff --git a/gnovm/cmd/gno/testdata/gno_test/test_with-native-fallback.txtar b/gnovm/cmd/gno/testdata/gno_test/test_with-native-fallback.txtar
deleted file mode 100644
index 0954d1dd932..00000000000
--- a/gnovm/cmd/gno/testdata/gno_test/test_with-native-fallback.txtar
+++ /dev/null
@@ -1,32 +0,0 @@
-# Test native lib
-
-! gno test -v .
-
-! stdout .+
-stderr 'panic: unknown import path net \[recovered\]'
-stderr ' panic: gno.land/r/\w{8}/contract.gno:3:1: unknown import path net'
-
-gno test -v --with-native-fallback .
-
-! stdout .+
-stderr '=== RUN TestFoo'
-stderr '--- PASS: TestFoo'
-
--- contract.gno --
-package contract
-
-import "net"
-
-func Foo() {
- _ = net.IPv4
-}
-
--- contract_test.gno --
-package contract
-
-import "testing"
-
-func TestFoo(t *testing.T) {
- Foo()
-}
-
diff --git a/gnovm/cmd/gno/testdata/gno_test/unknow_lib.txtar b/gnovm/cmd/gno/testdata/gno_test/unknow_lib.txtar
deleted file mode 100644
index 15125f695f5..00000000000
--- a/gnovm/cmd/gno/testdata/gno_test/unknow_lib.txtar
+++ /dev/null
@@ -1,32 +0,0 @@
-# Test unknow lib
-
-! gno test -v .
-
-! stdout .+
-stderr 'panic: unknown import path foobarbaz \[recovered\]'
-stderr ' panic: gno.land/r/\w{8}/contract.gno:3:1: unknown import path foobarbaz'
-
-! gno test -v --with-native-fallback .
-
-! stdout .+
-stderr 'panic: unknown import path foobarbaz \[recovered\]'
-stderr ' panic: gno.land/r/\w{8}/contract.gno:3:1: unknown import path foobarbaz'
-
--- contract.gno --
-package contract
-
-import "foobarbaz"
-
-func Foo() {
- _ = foobarbaz.Gnognogno
-}
-
--- contract_test.gno --
-package contract
-
-import "testing"
-
-func TestFoo(t *testing.T) {
- Foo()
-}
-
diff --git a/gnovm/cmd/gno/testdata/lint/bad_import.txtar b/gnovm/cmd/gno/testdata/lint/bad_import.txtar
new file mode 100644
index 00000000000..e2c0431443c
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/lint/bad_import.txtar
@@ -0,0 +1,22 @@
+# testing gno lint command: bad import error
+
+! gno lint ./bad_file.gno
+
+cmp stdout stdout.golden
+cmp stderr stderr.golden
+
+-- bad_file.gno --
+package main
+
+import "python"
+
+func main() {
+ println("Hello", 42)
+}
+
+-- gno.mod --
+module gno.land/p/test
+
+-- stdout.golden --
+-- stderr.golden --
+bad_file.gno:3:8: unknown import path python (code=2)
diff --git a/gnovm/cmd/gno/testdata/lint/file_error.txtar b/gnovm/cmd/gno/testdata/lint/file_error.txtar
new file mode 100644
index 00000000000..4fa50c6da81
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/lint/file_error.txtar
@@ -0,0 +1,23 @@
+# gno lint: test file error
+
+! gno lint ./i_have_error_test.gno
+
+cmp stdout stdout.golden
+cmp stderr stderr.golden
+
+-- i_have_error_test.gno --
+package main
+
+import "fmt"
+
+func TestIHaveSomeError() {
+ i := undefined_variable
+ fmt.Println("Hello", 42)
+}
+
+-- gno.mod --
+module gno.land/p/test
+
+-- stdout.golden --
+-- stderr.golden --
+i_have_error_test.gno:6:7: name undefined_variable not declared (code=2)
diff --git a/gnovm/cmd/gno/testdata/lint/no_error.txtar b/gnovm/cmd/gno/testdata/lint/no_error.txtar
new file mode 100644
index 00000000000..5dd3b164952
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/lint/no_error.txtar
@@ -0,0 +1,19 @@
+# testing simple gno lint command with any error
+
+gno lint ./good_file.gno
+
+cmp stdout stdout.golden
+cmp stdout stderr.golden
+
+-- good_file.gno --
+package main
+
+func main() {
+ println("Hello", 42)
+}
+
+-- gno.mod --
+module gno.land/p/demo/test
+
+-- stdout.golden --
+-- stderr.golden --
diff --git a/gnovm/cmd/gno/testdata/lint/no_gnomod.txtar b/gnovm/cmd/gno/testdata/lint/no_gnomod.txtar
new file mode 100644
index 00000000000..b5a046a7095
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/lint/no_gnomod.txtar
@@ -0,0 +1,17 @@
+# gno lint: no gnomod
+
+! gno lint .
+
+cmp stdout stdout.golden
+cmp stderr stderr.golden
+
+-- good_file.gno --
+package main
+
+func main() {
+ println("Hello", 42)
+}
+
+-- stdout.golden --
+-- stderr.golden --
+./.: parsing gno.mod at ./.: gno.mod file not found in current or any parent directory (code=1)
diff --git a/gnovm/cmd/gno/testdata/lint/not_declared.txtar b/gnovm/cmd/gno/testdata/lint/not_declared.txtar
new file mode 100644
index 00000000000..ac56b27e0df
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/lint/not_declared.txtar
@@ -0,0 +1,22 @@
+# testing gno lint command: not declared error
+
+! gno lint ./bad_file.gno
+
+cmp stdout stdout.golden
+cmp stderr stderr.golden
+
+-- bad_file.gno --
+package main
+
+func main() {
+ hello.Foo()
+ println("Hello", 42)
+}
+
+-- gno.mod --
+module gno.land/p/demo/hello
+
+-- stdout.golden --
+-- stderr.golden --
+bad_file.gno:4:2: undefined: hello (code=4)
+bad_file.gno:4:2: name hello not declared (code=2)
diff --git a/gnovm/cmd/gno/testdata/gno_test/dir_not_exist.txtar b/gnovm/cmd/gno/testdata/test/dir_not_exist.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/dir_not_exist.txtar
rename to gnovm/cmd/gno/testdata/test/dir_not_exist.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar b/gnovm/cmd/gno/testdata/test/empty_dir.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar
rename to gnovm/cmd/gno/testdata/test/empty_dir.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar b/gnovm/cmd/gno/testdata/test/empty_gno1.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar
rename to gnovm/cmd/gno/testdata/test/empty_gno1.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_gno2.txtar b/gnovm/cmd/gno/testdata/test/empty_gno2.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/empty_gno2.txtar
rename to gnovm/cmd/gno/testdata/test/empty_gno2.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_gno3.txtar b/gnovm/cmd/gno/testdata/test/empty_gno3.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/empty_gno3.txtar
rename to gnovm/cmd/gno/testdata/test/empty_gno3.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar b/gnovm/cmd/gno/testdata/test/error_correct.txtar
similarity index 86%
rename from gnovm/cmd/gno/testdata/gno_test/error_correct.txtar
rename to gnovm/cmd/gno/testdata/test/error_correct.txtar
index 20a399881be..f9ce4dd9028 100644
--- a/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar
+++ b/gnovm/cmd/gno/testdata/test/error_correct.txtar
@@ -2,7 +2,6 @@
gno test -v .
-stdout 'Machine\.RunMain\(\) panic: oups'
stderr '=== RUN file/x_filetest.gno'
stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)'
stderr 'ok \. \d\.\d\ds'
diff --git a/gnovm/cmd/gno/testdata/test/error_incorrect.txtar b/gnovm/cmd/gno/testdata/test/error_incorrect.txtar
new file mode 100644
index 00000000000..621397d8d1f
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/test/error_incorrect.txtar
@@ -0,0 +1,18 @@
+# Test Error instruction incorrect
+
+! gno test -v .
+
+stderr '=== RUN file/x_filetest.gno'
+stderr 'Error diff:'
+stderr '-xxx'
+stderr '\+oups'
+
+-- x_filetest.gno --
+package main
+
+func main() {
+ panic("oups")
+}
+
+// Error:
+// xxx
diff --git a/gnovm/cmd/gno/testdata/test/error_sync.txtar b/gnovm/cmd/gno/testdata/test/error_sync.txtar
new file mode 100644
index 00000000000..067489c41f2
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/test/error_sync.txtar
@@ -0,0 +1,29 @@
+# Test Error instruction updated
+# NOTE: unlike Output and Realm instruction updates, Error update is not driven
+# by the '-update-golden-tests' flag. The Error is only updated when it is
+# empty.
+
+gno test -update-golden-tests -v .
+
+! stdout .+
+stderr '=== RUN file/x_filetest.gno'
+
+cmp x_filetest.gno x_filetest.gno.golden
+
+-- x_filetest.gno --
+package main
+
+func main() {
+ panic("oups")
+}
+
+// Error:
+-- x_filetest.gno.golden --
+package main
+
+func main() {
+ panic("oups")
+}
+
+// Error:
+// oups
diff --git a/gnovm/cmd/gno/testdata/test/failing_filetest.txtar b/gnovm/cmd/gno/testdata/test/failing_filetest.txtar
new file mode 100644
index 00000000000..7b57729ee91
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/test/failing_filetest.txtar
@@ -0,0 +1,19 @@
+# Test with a failing _filetest.gno file
+
+! gno test -v .
+
+stderr '=== RUN file/failing_filetest.gno'
+stderr 'unexpected panic: beep boop'
+
+-- failing.gno --
+package failing
+
+-- failing_filetest.gno --
+package main
+
+func main() {
+ panic("beep boop")
+}
+
+// Output:
+// blah
diff --git a/gnovm/cmd/gno/testdata/gno_test/failing_test.txtar b/gnovm/cmd/gno/testdata/test/failing_test.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/failing_test.txtar
rename to gnovm/cmd/gno/testdata/test/failing_test.txtar
diff --git a/gnovm/cmd/gno/testdata/test/filetest_events.txtar b/gnovm/cmd/gno/testdata/test/filetest_events.txtar
new file mode 100644
index 00000000000..34da5fe2ff0
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/test/filetest_events.txtar
@@ -0,0 +1,51 @@
+# Test with a valid _filetest.gno file
+
+gno test -print-events .
+
+! stdout .+
+stderr 'ok \. \d\.\d\ds'
+
+gno test -print-events -v .
+
+stdout 'test'
+stderr '=== RUN file/valid_filetest.gno'
+stderr '--- PASS: file/valid_filetest.gno \(\d\.\d\ds\)'
+stderr 'ok \. \d\.\d\ds'
+
+-- valid.gno --
+package valid
+
+-- valid_filetest.gno --
+package main
+
+import "std"
+
+func main() {
+ println("test")
+ std.Emit("EventA")
+ std.Emit("EventB", "keyA", "valA")
+}
+
+// Output:
+// test
+
+// Events:
+// [
+// {
+// "type": "EventA",
+// "attrs": [],
+// "pkg_path": "",
+// "func": "main"
+// },
+// {
+// "type": "EventB",
+// "attrs": [
+// {
+// "key": "keyA",
+// "value": "valA"
+// }
+// ],
+// "pkg_path": "",
+// "func": "main"
+// }
+// ]
diff --git a/gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar b/gnovm/cmd/gno/testdata/test/flag_print-runtime-metrics.txtar
similarity index 75%
rename from gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar
rename to gnovm/cmd/gno/testdata/test/flag_print-runtime-metrics.txtar
index e065d00d55a..99747a0a241 100644
--- a/gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar
+++ b/gnovm/cmd/gno/testdata/test/flag_print-runtime-metrics.txtar
@@ -3,7 +3,7 @@
gno test --print-runtime-metrics .
! stdout .+
-stderr '--- runtime: cycle=[\d\.kM]+ imports=\d+ allocs=[\d\.kM]+\(\d\.\d\d%\)'
+stderr '--- runtime: cycle=[\d\.kM]+ allocs=[\d\.kM]+\(\d\.\d\d%\)'
-- metrics.gno --
package metrics
@@ -20,4 +20,3 @@ func TestTimeout(t *testing.T) {
println("plop")
}
}
-
diff --git a/gnovm/cmd/gno/testdata/gno_test/flag_run.txtar b/gnovm/cmd/gno/testdata/test/flag_run.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/flag_run.txtar
rename to gnovm/cmd/gno/testdata/test/flag_run.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_test/flag_timeout.txtar b/gnovm/cmd/gno/testdata/test/flag_timeout.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/flag_timeout.txtar
rename to gnovm/cmd/gno/testdata/test/flag_timeout.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_test/fmt_write_import.txtar b/gnovm/cmd/gno/testdata/test/fmt_write_import.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/fmt_write_import.txtar
rename to gnovm/cmd/gno/testdata/test/fmt_write_import.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_test/minim1.txtar b/gnovm/cmd/gno/testdata/test/minim1.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/minim1.txtar
rename to gnovm/cmd/gno/testdata/test/minim1.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_test/minim2.txtar b/gnovm/cmd/gno/testdata/test/minim2.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/minim2.txtar
rename to gnovm/cmd/gno/testdata/test/minim2.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_test/minim3.txtar b/gnovm/cmd/gno/testdata/test/minim3.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/minim3.txtar
rename to gnovm/cmd/gno/testdata/test/minim3.txtar
diff --git a/gnovm/cmd/gno/testdata/test/multitest_events.txtar b/gnovm/cmd/gno/testdata/test/multitest_events.txtar
new file mode 100644
index 00000000000..321c790561a
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/test/multitest_events.txtar
@@ -0,0 +1,26 @@
+# Test with a valid _test.gno file
+
+gno test -print-events .
+
+! stdout .+
+stderr 'EVENTS: \[{\"type\":\"EventA\",\"attrs\":\[\],\"pkg_path\":\"gno.land/r/.*\",\"func\":\"TestA\"}\]'
+stderr 'EVENTS: \[{\"type\":\"EventB\",\"attrs\":\[{\"key\":\"keyA\",\"value\":\"valA\"}\],\"pkg_path\":\"gno.land/r/.*\",\"func\":\"TestB\"},{\"type\":\"EventC\",\"attrs\":\[{\"key\":\"keyD\",\"value\":\"valD\"}\],\"pkg_path\":\"gno.land/r/.*\",\"func\":\"TestB\"}\]'
+stderr 'ok \. \d\.\d\ds'
+
+-- valid.gno --
+package valid
+
+-- valid_test.gno --
+package valid
+
+import "testing"
+import "std"
+
+func TestA(t *testing.T) {
+ std.Emit("EventA")
+}
+
+func TestB(t *testing.T) {
+ std.Emit("EventB", "keyA", "valA")
+ std.Emit("EventC", "keyD", "valD")
+}
diff --git a/gnovm/cmd/gno/testdata/test/no_path_empty_dir.txtar b/gnovm/cmd/gno/testdata/test/no_path_empty_dir.txtar
new file mode 100644
index 00000000000..6f8b54d7ea4
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/test/no_path_empty_dir.txtar
@@ -0,0 +1,6 @@
+# Run gno test without path argument on an empty dir
+
+gno test
+
+! stdout .+
+stderr '[no test files]'
\ No newline at end of file
diff --git a/gnovm/cmd/gno/testdata/test/no_path_empty_gno.txtar b/gnovm/cmd/gno/testdata/test/no_path_empty_gno.txtar
new file mode 100644
index 00000000000..846ce5bbd88
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/test/no_path_empty_gno.txtar
@@ -0,0 +1,8 @@
+# Test empty gno without path argument
+
+gno test
+
+! stdout .+
+stderr '\? \. \[no test files\]'
+
+-- empty.gno --
\ No newline at end of file
diff --git a/gnovm/cmd/gno/testdata/test/no_path_flag_run.txtar b/gnovm/cmd/gno/testdata/test/no_path_flag_run.txtar
new file mode 100644
index 00000000000..3db2a4c9295
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/test/no_path_flag_run.txtar
@@ -0,0 +1,99 @@
+# Run test on gno.land/p/demo/ufmt without path argument
+
+gno test
+
+gno test -v
+
+! stdout .+
+stderr '=== RUN TestRun/hello'
+stderr '=== RUN TestRun/hi_you'
+stderr '=== RUN TestRun/hi_me'
+stderr '=== RUN TestRun'
+stderr '--- PASS: TestRun'
+
+gno test -v -run .*
+
+! stdout .+
+stderr '=== RUN TestRun/hello'
+stderr '=== RUN TestRun/hi_you'
+stderr '=== RUN TestRun/hi_me'
+stderr '=== RUN TestRun'
+stderr '--- PASS: TestRun'
+
+gno test -v -run NotExists
+
+! stdout .+
+! stderr '=== RUN TestRun'
+
+gno test -v -run .*/hello
+
+! stdout .+
+stderr '=== RUN TestRun/hello'
+! stderr '=== RUN TestRun/hi_you'
+! stderr '=== RUN TestRun/hi_me'
+stderr '=== RUN TestRun'
+stderr '--- PASS: TestRun'
+
+gno test -v -run .*/hi
+
+! stdout .+
+! stderr '=== RUN TestRun/hello'
+stderr '=== RUN TestRun/hi_you'
+stderr '=== RUN TestRun/hi_me'
+stderr '=== RUN TestRun'
+stderr '--- PASS: TestRun'
+
+gno test -v -run .*/NotExists
+
+! stdout .+
+stderr '=== RUN TestRun'
+stderr '--- PASS: TestRun'
+
+gno test -v -run Run/.*
+
+! stdout .+
+stderr '=== RUN TestRun/hello'
+stderr '=== RUN TestRun/hi_you'
+stderr '=== RUN TestRun/hi_me'
+stderr '=== RUN TestRun'
+stderr '--- PASS: TestRun'
+
+gno test -v -run Run/
+
+! stdout .+
+stderr '=== RUN TestRun/hello'
+stderr '=== RUN TestRun/hi_you'
+stderr '=== RUN TestRun/hi_me'
+stderr '=== RUN TestRun'
+stderr '--- PASS: TestRun'
+
+gno test -v -run Run/hello
+
+! stdout .+
+stderr '=== RUN TestRun/hello'
+! stderr '=== RUN TestRun/hi_you'
+! stderr '=== RUN TestRun/hi_me'
+stderr '=== RUN TestRun'
+stderr '--- PASS: TestRun'
+
+-- run.gno --
+package run
+
+-- run_test.gno --
+package run
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestRun(t *testing.T) {
+ cases := []string {
+ "hello",
+ "hi you",
+ "hi me",
+ }
+ for _, tc := range cases {
+ t.Run(tc, func(t *testing.T) {})
+ }
+}
\ No newline at end of file
diff --git a/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar b/gnovm/cmd/gno/testdata/test/output_correct.txtar
similarity index 88%
rename from gnovm/cmd/gno/testdata/gno_test/output_correct.txtar
rename to gnovm/cmd/gno/testdata/test/output_correct.txtar
index e734dad7934..a8aa878e0a4 100644
--- a/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar
+++ b/gnovm/cmd/gno/testdata/test/output_correct.txtar
@@ -2,7 +2,8 @@
gno test -v .
-! stdout .+ # stdout should be empty
+stdout 'hey'
+stdout 'hru?'
stderr '=== RUN file/x_filetest.gno'
stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)'
stderr 'ok \. \d\.\d\ds'
diff --git a/gnovm/cmd/gno/testdata/test/output_incorrect.txtar b/gnovm/cmd/gno/testdata/test/output_incorrect.txtar
new file mode 100644
index 00000000000..60a38933d47
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/test/output_incorrect.txtar
@@ -0,0 +1,24 @@
+# Test Output instruction incorrect
+
+# with -v, stdout should contain output (unmodified).
+! gno test -v .
+
+stdout 'hey'
+
+stderr '=== RUN file/x_filetest.gno'
+stderr '--- Expected'
+stderr '\+\+\+ Actual'
+stderr '@@ -1,3 \+1,2 @@'
+stderr 'hey'
+stderr '-hru?'
+
+-- x_filetest.gno --
+package main
+
+func main() {
+ println("hey")
+}
+
+// Output:
+// hey
+// hru?
diff --git a/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar b/gnovm/cmd/gno/testdata/test/output_sync.txtar
similarity index 92%
rename from gnovm/cmd/gno/testdata/gno_test/output_sync.txtar
rename to gnovm/cmd/gno/testdata/test/output_sync.txtar
index 45e6e5c79be..45385a7eef9 100644
--- a/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar
+++ b/gnovm/cmd/gno/testdata/test/output_sync.txtar
@@ -2,7 +2,9 @@
gno test -v . -update-golden-tests
-! stdout .+ # stdout should be empty
+stdout 'hey'
+stdout '^hru\?'
+
stderr '=== RUN file/x_filetest.gno'
stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)'
stderr 'ok \. \d\.\d\ds'
@@ -19,7 +21,6 @@ func main() {
// Output:
// hey
-
-- x_filetest.gno.golden --
package main
@@ -31,4 +32,3 @@ func main() {
// Output:
// hey
// hru?
-
diff --git a/gnovm/cmd/gno/testdata/gno_test/panic.txtar b/gnovm/cmd/gno/testdata/test/panic.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/panic.txtar
rename to gnovm/cmd/gno/testdata/test/panic.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar b/gnovm/cmd/gno/testdata/test/pkg_underscore_test.txtar
similarity index 97%
rename from gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar
rename to gnovm/cmd/gno/testdata/test/pkg_underscore_test.txtar
index b38683adf81..7d204bdb98d 100644
--- a/gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar
+++ b/gnovm/cmd/gno/testdata/test/pkg_underscore_test.txtar
@@ -66,4 +66,5 @@ func main() {
println("filetest " + hello.Name)
}
-// Output: filetest foo
+// Output:
+// filetest foo
diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_boundmethod.txtar b/gnovm/cmd/gno/testdata/test/realm_boundmethod.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/realm_boundmethod.txtar
rename to gnovm/cmd/gno/testdata/test/realm_boundmethod.txtar
diff --git a/gnovm/cmd/gno/testdata/test/realm_correct.txtar b/gnovm/cmd/gno/testdata/test/realm_correct.txtar
new file mode 100644
index 00000000000..ae1212133fd
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/test/realm_correct.txtar
@@ -0,0 +1,21 @@
+# Test Realm instruction correct
+
+gno test -v .
+
+! stdout .+ # stdout should be empty
+stderr '=== RUN file/x_filetest.gno'
+stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)'
+stderr 'ok \. \d\.\d\ds'
+
+-- x_filetest.gno --
+// PKGPATH: gno.land/r/xx
+package xx
+
+var x int
+
+func main() {
+ x = 1
+}
+
+// Realm:
+// switchrealm["gno.land/r/xx"]
\ No newline at end of file
diff --git a/gnovm/cmd/gno/testdata/test/realm_incorrect.txtar b/gnovm/cmd/gno/testdata/test/realm_incorrect.txtar
new file mode 100644
index 00000000000..84f4e3438ee
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/test/realm_incorrect.txtar
@@ -0,0 +1,26 @@
+# Test Realm instruction incorrect
+
+! gno test -v .
+
+! stdout .+ # stdout should be empty
+stderr '=== RUN file/x_filetest.gno'
+stderr 'Realm diff:'
+stderr '--- Expected'
+stderr '\+\+\+ Actual'
+stderr '@@ -1,2 \+1,2 @@'
+stderr '-xxx'
+stderr '\+switchrealm\["gno.land/r/xx"\]'
+stderr 'x_filetest.gno failed'
+
+-- x_filetest.gno --
+// PKGPATH: gno.land/r/xx
+package xx
+
+var x int
+
+func main() {
+ x = 1
+}
+
+// Realm:
+// xxxx
diff --git a/gnovm/cmd/gno/testdata/test/realm_sync.txtar b/gnovm/cmd/gno/testdata/test/realm_sync.txtar
new file mode 100644
index 00000000000..65a930b2f03
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/test/realm_sync.txtar
@@ -0,0 +1,35 @@
+# Test Realm instruction updated
+
+gno test -v . -update-golden-tests
+
+! stdout .+ # stdout should be empty
+stderr '=== RUN file/x_filetest.gno'
+stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)'
+stderr 'ok \. \d\.\d\ds'
+
+cmp x_filetest.gno x_filetest.gno.golden
+
+-- x_filetest.gno --
+// PKGPATH: gno.land/r/xx
+package xx
+
+var x int
+
+func main() {
+ x = 1
+}
+
+// Realm:
+// xxx
+-- x_filetest.gno.golden --
+// PKGPATH: gno.land/r/xx
+package xx
+
+var x int
+
+func main() {
+ x = 1
+}
+
+// Realm:
+// switchrealm["gno.land/r/xx"]
diff --git a/gnovm/cmd/gno/testdata/gno_test/recover.txtar b/gnovm/cmd/gno/testdata/test/recover.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/recover.txtar
rename to gnovm/cmd/gno/testdata/test/recover.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_test/skip.txtar b/gnovm/cmd/gno/testdata/test/skip.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/skip.txtar
rename to gnovm/cmd/gno/testdata/test/skip.txtar
diff --git a/gnovm/cmd/gno/testdata/test/unknown_package.txtar b/gnovm/cmd/gno/testdata/test/unknown_package.txtar
new file mode 100644
index 00000000000..0611d3440a4
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/test/unknown_package.txtar
@@ -0,0 +1,24 @@
+# Test for loading an unknown package
+
+! gno test -v .
+
+! stdout .+
+stderr 'contract.gno:3:8: unknown import path foobarbaz'
+
+-- contract.gno --
+package contract
+
+import "foobarbaz"
+
+func Foo() {
+ _ = foobarbaz.Gnognogno
+}
+
+-- contract_test.gno --
+package contract
+
+import "testing"
+
+func TestFoo(t *testing.T) {
+ Foo()
+}
diff --git a/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar b/gnovm/cmd/gno/testdata/test/valid_filetest.txtar
similarity index 96%
rename from gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar
rename to gnovm/cmd/gno/testdata/test/valid_filetest.txtar
index 02ae3f72304..4e24ad9ab08 100644
--- a/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar
+++ b/gnovm/cmd/gno/testdata/test/valid_filetest.txtar
@@ -7,7 +7,7 @@ stderr 'ok \. \d\.\d\ds'
gno test -v .
-! stdout .+
+stdout 'test'
stderr '=== RUN file/valid_filetest.gno'
stderr '--- PASS: file/valid_filetest.gno \(\d\.\d\ds\)'
stderr 'ok \. \d\.\d\ds'
diff --git a/gnovm/cmd/gno/testdata/gno_test/valid_test.txtar b/gnovm/cmd/gno/testdata/test/valid_test.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_test/valid_test.txtar
rename to gnovm/cmd/gno/testdata/test/valid_test.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar b/gnovm/cmd/gno/testdata/transpile/gobuild_flag_build_error.txtar
similarity index 78%
rename from gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar
rename to gnovm/cmd/gno/testdata/transpile/gobuild_flag_build_error.txtar
index d21390f9472..145fe796c09 100644
--- a/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar
+++ b/gnovm/cmd/gno/testdata/transpile/gobuild_flag_build_error.txtar
@@ -1,10 +1,11 @@
# Run gno transpile with -gobuild flag
+# The error messages changed sometime in go1.23, so this avoids errors
! gno transpile -gobuild .
! stdout .+
-stderr '^main.gno:4:6: x declared and not used$'
-stderr '^main.gno:5:6: y declared and not used$'
+stderr '^main.gno:4:6: .*declared and not used'
+stderr '^main.gno:5:6: .*declared and not used'
stderr '^2 transpile error\(s\)$'
cmp main.gno.gen.go main.gno.gen.go.golden
diff --git a/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_parse_error.txtar b/gnovm/cmd/gno/testdata/transpile/gobuild_flag_parse_error.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_parse_error.txtar
rename to gnovm/cmd/gno/testdata/transpile/gobuild_flag_parse_error.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_transpile/invalid_import.txtar b/gnovm/cmd/gno/testdata/transpile/invalid_import.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_transpile/invalid_import.txtar
rename to gnovm/cmd/gno/testdata/transpile/invalid_import.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_transpile/no_args.txtar b/gnovm/cmd/gno/testdata/transpile/no_args.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_transpile/no_args.txtar
rename to gnovm/cmd/gno/testdata/transpile/no_args.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_transpile/parse_error.txtar b/gnovm/cmd/gno/testdata/transpile/parse_error.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_transpile/parse_error.txtar
rename to gnovm/cmd/gno/testdata/transpile/parse_error.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_empty_dir.txtar b/gnovm/cmd/gno/testdata/transpile/valid_empty_dir.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_transpile/valid_empty_dir.txtar
rename to gnovm/cmd/gno/testdata/transpile/valid_empty_dir.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_file.txtar b/gnovm/cmd/gno/testdata/transpile/valid_gobuild_file.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_file.txtar
rename to gnovm/cmd/gno/testdata/transpile/valid_gobuild_file.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_flag.txtar b/gnovm/cmd/gno/testdata/transpile/valid_gobuild_flag.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_flag.txtar
rename to gnovm/cmd/gno/testdata/transpile/valid_gobuild_flag.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_output_flag.txtar b/gnovm/cmd/gno/testdata/transpile/valid_output_flag.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_transpile/valid_output_flag.txtar
rename to gnovm/cmd/gno/testdata/transpile/valid_output_flag.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_output_gobuild.txtar b/gnovm/cmd/gno/testdata/transpile/valid_output_gobuild.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_transpile/valid_output_gobuild.txtar
rename to gnovm/cmd/gno/testdata/transpile/valid_output_gobuild.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_file.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_file.txtar
rename to gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_package.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_package.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_package.txtar
rename to gnovm/cmd/gno/testdata/transpile/valid_transpile_package.txtar
diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_tree.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_tree.txtar
similarity index 100%
rename from gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_tree.txtar
rename to gnovm/cmd/gno/testdata/transpile/valid_transpile_tree.txtar
diff --git a/gnovm/cmd/gno/testdata_test.go b/gnovm/cmd/gno/testdata_test.go
index 15bc8d96e26..c5cb0def04e 100644
--- a/gnovm/cmd/gno/testdata_test.go
+++ b/gnovm/cmd/gno/testdata_test.go
@@ -3,7 +3,6 @@ package main
import (
"os"
"path/filepath"
- "strconv"
"testing"
"github.com/gnolang/gno/gnovm/pkg/integration"
@@ -18,6 +17,7 @@ func Test_Scripts(t *testing.T) {
testdirs, err := os.ReadDir(testdata)
require.NoError(t, err)
+ homeDir, buildDir := t.TempDir(), t.TempDir()
for _, dir := range testdirs {
if !dir.IsDir() {
continue
@@ -26,18 +26,14 @@ func Test_Scripts(t *testing.T) {
name := dir.Name()
t.Logf("testing: %s", name)
t.Run(name, func(t *testing.T) {
- updateScripts, _ := strconv.ParseBool(os.Getenv("UPDATE_SCRIPTS"))
- p := testscript.Params{
- UpdateScripts: updateScripts,
- Dir: filepath.Join(testdata, name),
- }
-
+ testdir := filepath.Join(testdata, name)
+ p := integration.NewTestingParams(t, testdir)
if coverdir, ok := integration.ResolveCoverageDir(); ok {
err := integration.SetupTestscriptsCoverage(&p, coverdir)
require.NoError(t, err)
}
- err := integration.SetupGno(&p, t.TempDir())
+ err := integration.SetupGno(&p, homeDir, buildDir)
require.NoError(t, err)
testscript.Run(t, p)
diff --git a/gnovm/cmd/gno/transpile_test.go b/gnovm/cmd/gno/transpile_test.go
index 827c09e23f1..5a03ddc7657 100644
--- a/gnovm/cmd/gno/transpile_test.go
+++ b/gnovm/cmd/gno/transpile_test.go
@@ -6,29 +6,9 @@ import (
"strconv"
"testing"
- "github.com/rogpeppe/go-internal/testscript"
"github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/gnolang/gno/gnovm/pkg/integration"
)
-func Test_ScriptsTranspile(t *testing.T) {
- p := testscript.Params{
- Dir: "testdata/gno_transpile",
- }
-
- if coverdir, ok := integration.ResolveCoverageDir(); ok {
- err := integration.SetupTestscriptsCoverage(&p, coverdir)
- require.NoError(t, err)
- }
-
- err := integration.SetupGno(&p, t.TempDir())
- require.NoError(t, err)
-
- testscript.Run(t, p)
-}
-
func Test_parseGoBuildErrors(t *testing.T) {
t.Parallel()
diff --git a/gnovm/cmd/gno/util.go b/gnovm/cmd/gno/util.go
index 90aedd5d27a..697aa94b3c6 100644
--- a/gnovm/cmd/gno/util.go
+++ b/gnovm/cmd/gno/util.go
@@ -338,17 +338,3 @@ func copyFile(src, dst string) error {
return nil
}
-
-// Adapted from https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
-func prettySize(nb int64) string {
- const unit = 1000
- if nb < unit {
- return fmt.Sprintf("%d", nb)
- }
- div, exp := int64(unit), 0
- for n := nb / unit; n >= unit; n /= unit {
- div *= unit
- exp++
- }
- return fmt.Sprintf("%.1f%c", float64(nb)/float64(div), "kMGTPE"[exp])
-}
diff --git a/gnovm/gno.proto b/gnovm/gno.proto
index 5f53c363b73..8a15ca96e14 100644
--- a/gnovm/gno.proto
+++ b/gnovm/gno.proto
@@ -1,7 +1,7 @@
syntax = "proto3";
package gno;
-option go_package = "github.com/gnolang/gno/pb";
+option go_package = "github.com/gnolang/gno/gnovm/pb";
// imports
import "google/protobuf/any.proto";
@@ -601,3 +601,15 @@ message tupleType {
message RefType {
string ID = 1;
}
+
+// messages
+message MemFile {
+ string name = 1;
+ string body = 2;
+}
+
+message MemPackage {
+ string name = 1;
+ string path = 2;
+ repeated MemFile files = 3;
+}
diff --git a/gnovm/gnovm.proto b/gnovm/gnovm.proto
new file mode 100644
index 00000000000..c9f0b23ae80
--- /dev/null
+++ b/gnovm/gnovm.proto
@@ -0,0 +1,16 @@
+syntax = "proto3";
+package gnovm;
+
+option go_package = "github.com/gnolang/gno/gnovm/pb";
+
+// messages
+message MemFile {
+ string name = 1;
+ string body = 2;
+}
+
+message MemPackage {
+ string name = 1;
+ string path = 2;
+ repeated MemFile files = 3;
+}
\ No newline at end of file
diff --git a/tm2/pkg/std/memfile.go b/gnovm/memfile.go
similarity index 95%
rename from tm2/pkg/std/memfile.go
rename to gnovm/memfile.go
index 01bc18c1487..6988c893dd7 100644
--- a/tm2/pkg/std/memfile.go
+++ b/gnovm/memfile.go
@@ -1,4 +1,4 @@
-package std
+package gnovm
import (
"fmt"
@@ -41,7 +41,7 @@ const pathLengthLimit = 256
var (
rePkgName = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)
- rePkgOrRlmPath = regexp.MustCompile(`^gno\.land\/(?:p|r)(?:\/_?[a-z]+[a-z0-9_]*)+$`)
+ rePkgOrRlmPath = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/(?:p|r)(?:\/_?[a-z]+[a-z0-9_]*)+$`)
reFileName = regexp.MustCompile(`^([a-zA-Z0-9_]*\.[a-z0-9_\.]*|LICENSE|README)$`)
)
diff --git a/tm2/pkg/std/memfile_test.go b/gnovm/memfile_test.go
similarity index 99%
rename from tm2/pkg/std/memfile_test.go
rename to gnovm/memfile_test.go
index 3e1fb49e131..5ef70e9e868 100644
--- a/tm2/pkg/std/memfile_test.go
+++ b/gnovm/memfile_test.go
@@ -1,4 +1,4 @@
-package std
+package gnovm
import (
"testing"
@@ -158,13 +158,13 @@ func TestMemPackage_Validate(t *testing.T) {
"invalid package/realm path",
},
{
- "Invalid path",
+ "Custom domain",
&MemPackage{
Name: "hey",
Path: "github.com/p/path/path",
Files: []*MemFile{{Name: "a.gno"}},
},
- "invalid package/realm path",
+ "",
},
{
"Special character",
diff --git a/gnovm/package.go b/gnovm/package.go
new file mode 100644
index 00000000000..d6332b05709
--- /dev/null
+++ b/gnovm/package.go
@@ -0,0 +1,15 @@
+package gnovm
+
+import (
+ "github.com/gnolang/gno/tm2/pkg/amino"
+)
+
+var Package = amino.RegisterPackage(amino.NewPackage(
+ "github.com/gnolang/gno/gnovm",
+ "gnovm",
+ amino.GetCallersDirname(),
+).WithDependencies().WithTypes(
+ // MemFile/MemPackage
+ MemFile{}, "MemFile",
+ MemPackage{}, "MemPackage",
+))
diff --git a/gnovm/pkg/benchops/bench.go b/gnovm/pkg/benchops/bench.go
new file mode 100644
index 00000000000..305712debcf
--- /dev/null
+++ b/gnovm/pkg/benchops/bench.go
@@ -0,0 +1,116 @@
+package benchops
+
+import (
+ "time"
+)
+
+const (
+ invalidCode = byte(0x00)
+)
+
+var measure bench
+
+type bench struct {
+ opCounts [256]int64
+ opAccumDur [256]time.Duration
+ opStartTime [256]time.Time
+ isOpCodeStarted bool
+ curOpCode byte
+ timeZero time.Time
+
+ storeCounts [256]int64
+ storeAccumDur [256]time.Duration
+ storeAccumSize [256]int64
+ storeStartTime [256]time.Time
+ curStoreCode byte
+}
+
+func InitMeasure() {
+ measure = bench{
+ // this will be called to reset each benchmarking
+ isOpCodeStarted: false,
+ curOpCode: invalidCode,
+ curStoreCode: invalidCode,
+ }
+}
+
+func StartOpCode(code byte) {
+ if code == invalidCode {
+ panic("the OpCode is invalid")
+ }
+ if measure.opStartTime[code] != measure.timeZero {
+ panic("Can not start a non-stopped timer")
+ }
+ measure.opStartTime[code] = time.Now()
+ measure.opCounts[code]++
+
+ measure.isOpCodeStarted = true
+ measure.curOpCode = code
+}
+
+// Stop the current measurement
+func StopOpCode() {
+ code := measure.curOpCode
+ if measure.opStartTime[code] == measure.timeZero {
+ panic("Can not stop a stopped timer")
+ }
+ measure.opAccumDur[code] += time.Since(measure.opStartTime[code])
+ measure.opStartTime[code] = measure.timeZero // stop the timer
+ measure.isOpCodeStarted = false
+}
+
+// Pause current opcode measurement
+func PauseOpCode() {
+ if measure.isOpCodeStarted == false {
+ return
+ }
+ if measure.curOpCode == invalidCode {
+ panic("Can not Pause timer of an invalid OpCode")
+ }
+ code := measure.curOpCode
+ if measure.opStartTime[code] == measure.timeZero {
+ panic("Should not pause a stopped timer")
+ }
+ measure.opAccumDur[code] += time.Since(measure.opStartTime[code])
+ measure.opStartTime[code] = measure.timeZero
+}
+
+// Resume resumes current measurement
+func ResumeOpCode() {
+ if measure.isOpCodeStarted == false {
+ return
+ }
+ if measure.curOpCode == invalidCode {
+ panic("Can not resume timer of an invalid OpCode")
+ }
+
+ code := measure.curOpCode
+
+ if measure.opStartTime[code] != measure.timeZero {
+ panic("Should not resume a running timer")
+ }
+ measure.opStartTime[code] = time.Now()
+}
+
+func StartStore(code byte) {
+ if measure.storeStartTime[code] != measure.timeZero {
+ panic("Can not start a non-stopped timer")
+ }
+ measure.storeStartTime[code] = time.Now()
+ measure.storeCounts[code]++
+ measure.curStoreCode = code
+}
+
+// assume there is no recursive call for store.
+func StopStore(size int) {
+ code := measure.curStoreCode
+
+ if measure.storeStartTime[code] == measure.timeZero {
+ panic("Can not stop a stopped timer")
+ }
+
+ measure.storeAccumDur[code] += time.Since(measure.storeStartTime[code])
+ measure.storeStartTime[code] = measure.timeZero // stop the timer
+ measure.storeAccumSize[code] += int64(size)
+ measure.curStoreCode = invalidCode
+}
diff --git a/gnovm/pkg/benchops/exporter.go b/gnovm/pkg/benchops/exporter.go
new file mode 100644
index 00000000000..3f0e21a1793
--- /dev/null
+++ b/gnovm/pkg/benchops/exporter.go
@@ -0,0 +1,109 @@
+package benchops
+
+import (
+ "encoding/binary"
+ "log"
+ "math"
+ "os"
+ "time"
+)
+
+// the byte size of a exported record
+const RecordSize int = 10
+
+var fileWriter *exporter
+
+func initExporter(fileName string) {
+ file, err := os.Create(fileName)
+ if err != nil {
+ panic("could not create benchmark file: " + err.Error())
+ }
+
+ fileWriter = &exporter{
+ file: file,
+ }
+}
+
+type exporter struct {
+ file *os.File
+}
+
+// export code, duration, size in a 10 bytes record
+// byte 1: OpCode
+// byte 2: StoreCode
+// byte 3-6: Duration
+// byte 7-10: Size
+func (e *exporter) export(code Code, elapsedTime time.Duration, size int64) {
+ // the MaxUint32 is 4294967295. It represents 4.29 seconds in duration or 4G bytes.
+ // It panics not only for overflow protection, but also for abnormal measurements.
+ if elapsedTime > math.MaxUint32 {
+ log.Fatalf("elapsedTime %d out of uint32 range", elapsedTime)
+ }
+ if size > math.MaxUint32 {
+ log.Fatalf("size %d out of uint32 range", size)
+ }
+
+ buf := []byte{code[0], code[1], 0, 0, 0, 0, 0, 0, 0, 0}
+ binary.LittleEndian.PutUint32(buf[2:], uint32(elapsedTime))
+ binary.LittleEndian.PutUint32(buf[6:], uint32(size))
+ _, err := e.file.Write(buf)
+ if err != nil {
+ panic("could not write to benchmark file: " + err.Error())
+ }
+}
+
+func (e *exporter) close() {
+ e.file.Sync()
+ e.file.Close()
+}
+
+func FinishStore() {
+ for i := 0; i < 256; i++ {
+ count := measure.storeCounts[i]
+
+ if count == 0 {
+ continue
+ }
+ // check unstopped timer
+ if measure.storeStartTime[i] != measure.timeZero {
+ panic("timer should have stopped before FinishRun")
+ }
+
+ code := [2]byte{0x00, byte(i)}
+
+ fileWriter.export(
+ code,
+ measure.storeAccumDur[i]/time.Duration(count),
+ measure.storeAccumSize[i]/count,
+ )
+ }
+}
+
+func FinishRun() {
+ for i := 0; i < 256; i++ {
+ if measure.opCounts[i] == 0 {
+ continue
+ }
+ // check unstopped timer
+ if measure.opStartTime[i] != measure.timeZero {
+ panic("timer should have stopped before FinishRun")
+ }
+
+ code := [2]byte{byte(i), 0x00}
+ fileWriter.export(code, measure.opAccumDur[i]/time.Duration(measure.opCounts[i]), 0)
+ }
+ ResetRun()
+}
+
+// It reset each machine Runs
+func ResetRun() {
+ measure.opCounts = [256]int64{}
+ measure.opAccumDur = [256]time.Duration{}
+ measure.opStartTime = [256]time.Time{}
+ measure.curOpCode = invalidCode
+ measure.isOpCodeStarted = false
+}
+
+func Finish() {
+ fileWriter.close()
+}
diff --git a/gnovm/pkg/benchops/gno/avl/gno.mod b/gnovm/pkg/benchops/gno/avl/gno.mod
new file mode 100644
index 00000000000..a6a2a1362e3
--- /dev/null
+++ b/gnovm/pkg/benchops/gno/avl/gno.mod
@@ -0,0 +1 @@
+module gno.land/p/demo/avl
diff --git a/gnovm/pkg/benchops/gno/avl/node.gno b/gnovm/pkg/benchops/gno/avl/node.gno
new file mode 100644
index 00000000000..7308e163768
--- /dev/null
+++ b/gnovm/pkg/benchops/gno/avl/node.gno
@@ -0,0 +1,487 @@
+package avl
+
+//----------------------------------------
+// Node
+
+// Node represents a node in an AVL tree.
+type Node struct {
+ key string // key is the unique identifier for the node.
+ value interface{} // value is the data stored in the node.
+ height int8 // height is the height of the node in the tree.
+ size int // size is the number of nodes in the subtree rooted at this node.
+ leftNode *Node // leftNode is the left child of the node.
+ rightNode *Node // rightNode is the right child of the node.
+}
+
+// NewNode creates a new node with the given key and value.
+func NewNode(key string, value interface{}) *Node {
+ return &Node{
+ key: key,
+ value: value,
+ height: 0,
+ size: 1,
+ }
+}
+
+// Size returns the size of the subtree rooted at the node.
+func (node *Node) Size() int {
+ if node == nil {
+ return 0
+ }
+ return node.size
+}
+
+// IsLeaf checks if the node is a leaf node (has no children).
+func (node *Node) IsLeaf() bool {
+ return node.height == 0
+}
+
+// Key returns the key of the node.
+func (node *Node) Key() string {
+ return node.key
+}
+
+// Value returns the value of the node.
+func (node *Node) Value() interface{} {
+ return node.value
+}
+
+// _copy creates a copy of the node (excluding value).
+func (node *Node) _copy() *Node {
+ if node.height == 0 {
+ panic("Why are you copying a value node?")
+ }
+ return &Node{
+ key: node.key,
+ height: node.height,
+ size: node.size,
+ leftNode: node.leftNode,
+ rightNode: node.rightNode,
+ }
+}
+
+// Has checks if a node with the given key exists in the subtree rooted at the node.
+func (node *Node) Has(key string) (has bool) {
+ if node == nil {
+ return false
+ }
+ if node.key == key {
+ return true
+ }
+ if node.height == 0 {
+ return false
+ }
+ if key < node.key {
+ return node.getLeftNode().Has(key)
+ }
+ return node.getRightNode().Has(key)
+}
+
+// Get searches for a node with the given key in the subtree rooted at the node
+// and returns its index, value, and whether it exists.
+func (node *Node) Get(key string) (index int, value interface{}, exists bool) {
+ if node == nil {
+ return 0, nil, false
+ }
+
+ if node.height == 0 {
+ if node.key == key {
+ return 0, node.value, true
+ }
+ if node.key < key {
+ return 1, nil, false
+ }
+ return 0, nil, false
+ }
+
+ if key < node.key {
+ return node.getLeftNode().Get(key)
+ }
+
+ rightNode := node.getRightNode()
+ index, value, exists = rightNode.Get(key)
+ index += node.size - rightNode.size
+ return index, value, exists
+}
+
+// GetByIndex retrieves the key-value pair of the node at the given index
+// in the subtree rooted at the node.
+func (node *Node) GetByIndex(index int) (key string, value interface{}) {
+ if node.height == 0 {
+ if index == 0 {
+ return node.key, node.value
+ }
+ panic("GetByIndex asked for invalid index")
+ }
+ // TODO: could improve this by storing the sizes
+ leftNode := node.getLeftNode()
+ if index < leftNode.size {
+ return leftNode.GetByIndex(index)
+ }
+ return node.getRightNode().GetByIndex(index - leftNode.size)
+}
+
+// Set inserts a new node with the given key-value pair into the subtree rooted at the node,
+// and returns the new root of the subtree and whether an existing node was updated.
+//
+// XXX consider a better way to do this... perhaps split Node from Node.
+func (node *Node) Set(key string, value interface{}) (newSelf *Node, updated bool) {
+ if node == nil {
+ return NewNode(key, value), false
+ }
+
+ if node.height == 0 {
+ return node.setLeaf(key, value)
+ }
+
+ node = node._copy()
+ if key < node.key {
+ node.leftNode, updated = node.getLeftNode().Set(key, value)
+ } else {
+ node.rightNode, updated = node.getRightNode().Set(key, value)
+ }
+
+ if updated {
+ return node, updated
+ }
+
+ node.calcHeightAndSize()
+ return node.balance(), updated
+}
+
+// setLeaf inserts a new leaf node with the given key-value pair into the subtree rooted at the node,
+// and returns the new root of the subtree and whether an existing node was updated.
+func (node *Node) setLeaf(key string, value interface{}) (newSelf *Node, updated bool) {
+ if key == node.key {
+ return NewNode(key, value), true
+ }
+
+ if key < node.key {
+ return &Node{
+ key: node.key,
+ height: 1,
+ size: 2,
+ leftNode: NewNode(key, value),
+ rightNode: node,
+ }, false
+ }
+
+ return &Node{
+ key: key,
+ height: 1,
+ size: 2,
+ leftNode: node,
+ rightNode: NewNode(key, value),
+ }, false
+}
+
+// Remove deletes the node with the given key from the subtree rooted at the node.
+// returns the new root of the subtree, the new leftmost leaf key (if changed),
+// the removed value and the removal was successful.
+func (node *Node) Remove(key string) (
+ newNode *Node, newKey string, value interface{}, removed bool,
+) {
+ if node == nil {
+ return nil, "", nil, false
+ }
+ if node.height == 0 {
+ if key == node.key {
+ return nil, "", node.value, true
+ }
+ return node, "", nil, false
+ }
+ if key < node.key {
+ var newLeftNode *Node
+ newLeftNode, newKey, value, removed = node.getLeftNode().Remove(key)
+ if !removed {
+ return node, "", value, false
+ }
+ if newLeftNode == nil { // left node held value, was removed
+ return node.rightNode, node.key, value, true
+ }
+ node = node._copy()
+ node.leftNode = newLeftNode
+ node.calcHeightAndSize()
+ node = node.balance()
+ return node, newKey, value, true
+ }
+
+ var newRightNode *Node
+ newRightNode, newKey, value, removed = node.getRightNode().Remove(key)
+ if !removed {
+ return node, "", value, false
+ }
+ if newRightNode == nil { // right node held value, was removed
+ return node.leftNode, "", value, true
+ }
+ node = node._copy()
+ node.rightNode = newRightNode
+ if newKey != "" {
+ node.key = newKey
+ }
+ node.calcHeightAndSize()
+ node = node.balance()
+ return node, "", value, true
+}
+
+// getLeftNode returns the left child of the node.
+func (node *Node) getLeftNode() *Node {
+ return node.leftNode
+}
+
+// getRightNode returns the right child of the node.
+func (node *Node) getRightNode() *Node {
+ return node.rightNode
+}
+
+// rotateRight performs a right rotation on the node and returns the new root.
+// NOTE: overwrites node
+// TODO: optimize balance & rotate
+func (node *Node) rotateRight() *Node {
+ node = node._copy()
+ l := node.getLeftNode()
+ _l := l._copy()
+
+ _lrCached := _l.rightNode
+ _l.rightNode = node
+ node.leftNode = _lrCached
+
+ node.calcHeightAndSize()
+ _l.calcHeightAndSize()
+
+ return _l
+}
+
+// rotateLeft performs a left rotation on the node and returns the new root.
+// NOTE: overwrites node
+// TODO: optimize balance & rotate
+func (node *Node) rotateLeft() *Node {
+ node = node._copy()
+ r := node.getRightNode()
+ _r := r._copy()
+
+ _rlCached := _r.leftNode
+ _r.leftNode = node
+ node.rightNode = _rlCached
+
+ node.calcHeightAndSize()
+ _r.calcHeightAndSize()
+
+ return _r
+}
+
+// calcHeightAndSize updates the height and size of the node based on its children.
+// NOTE: mutates height and size
+func (node *Node) calcHeightAndSize() {
+ node.height = maxInt8(node.getLeftNode().height, node.getRightNode().height) + 1
+ node.size = node.getLeftNode().size + node.getRightNode().size
+}
+
+// calcBalance calculates the balance factor of the node.
+func (node *Node) calcBalance() int {
+ return int(node.getLeftNode().height) - int(node.getRightNode().height)
+}
+
+// balance balances the subtree rooted at the node and returns the new root.
+// NOTE: assumes that node can be modified
+// TODO: optimize balance & rotate
+func (node *Node) balance() (newSelf *Node) {
+ balance := node.calcBalance()
+ if balance >= -1 {
+ return node
+ }
+ if balance > 1 {
+ if node.getLeftNode().calcBalance() >= 0 {
+ // Left Left Case
+ return node.rotateRight()
+ }
+ // Left Right Case
+ left := node.getLeftNode()
+ node.leftNode = left.rotateLeft()
+ return node.rotateRight()
+ }
+
+ if node.getRightNode().calcBalance() <= 0 {
+ // Right Right Case
+ return node.rotateLeft()
+ }
+
+ // Right Left Case
+ right := node.getRightNode()
+ node.rightNode = right.rotateRight()
+ return node.rotateLeft()
+}
+
+// Shortcut for TraverseInRange.
+func (node *Node) Iterate(start, end string, cb func(*Node) bool) bool {
+ return node.TraverseInRange(start, end, true, true, cb)
+}
+
+// Shortcut for TraverseInRange.
+func (node *Node) ReverseIterate(start, end string, cb func(*Node) bool) bool {
+ return node.TraverseInRange(start, end, false, true, cb)
+}
+
+// TraverseInRange traverses all nodes, including inner nodes.
+// Start is inclusive and end is exclusive when ascending,
+// Start and end are inclusive when descending.
+// Empty start and empty end denote no start and no end.
+// If leavesOnly is true, only visit leaf nodes.
+// NOTE: To simulate an exclusive reverse traversal,
+// just append 0x00 to start.
+func (node *Node) TraverseInRange(start, end string, ascending bool, leavesOnly bool, cb func(*Node) bool) bool {
+ if node == nil {
+ return false
+ }
+ afterStart := (start == "" || start < node.key)
+ startOrAfter := (start == "" || start <= node.key)
+ beforeEnd := false
+ if ascending {
+ beforeEnd = (end == "" || node.key < end)
+ } else {
+ beforeEnd = (end == "" || node.key <= end)
+ }
+
+ // Run callback per inner/leaf node.
+ stop := false
+ if (!node.IsLeaf() && !leavesOnly) ||
+ (node.IsLeaf() && startOrAfter && beforeEnd) {
+ stop = cb(node)
+ if stop {
+ return stop
+ }
+ }
+ if node.IsLeaf() {
+ return stop
+ }
+
+ if ascending {
+ // check lower nodes, then higher
+ if afterStart {
+ stop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb)
+ }
+ if stop {
+ return stop
+ }
+ if beforeEnd {
+ stop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb)
+ }
+ } else {
+ // check the higher nodes first
+ if beforeEnd {
+ stop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb)
+ }
+ if stop {
+ return stop
+ }
+ if afterStart {
+ stop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb)
+ }
+ }
+
+ return stop
+}
+
+// TraverseByOffset traverses all nodes, including inner nodes.
+// A limit of math.MaxInt means no limit.
+func (node *Node) TraverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool {
+ if node == nil {
+ return false
+ }
+
+ // fast paths. these happen only if TraverseByOffset is called directly on a leaf.
+ if limit <= 0 || offset >= node.size {
+ return false
+ }
+ if node.IsLeaf() {
+ if offset > 0 {
+ return false
+ }
+ return cb(node)
+ }
+
+ // go to the actual recursive function.
+ return node.traverseByOffset(offset, limit, descending, leavesOnly, cb)
+}
+
+// TraverseByOffset traverses the subtree rooted at the node by offset and limit,
+// in either ascending or descending order, and applies the callback function to each traversed node.
+// If leavesOnly is true, only leaf nodes are visited.
+func (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool {
+ // caller guarantees: offset < node.size; limit > 0.
+ if !leavesOnly {
+ if cb(node) {
+ return true
+ }
+ }
+ first, second := node.getLeftNode(), node.getRightNode()
+ if descending {
+ first, second = second, first
+ }
+ if first.IsLeaf() {
+ // either run or skip, based on offset
+ if offset > 0 {
+ offset--
+ } else {
+ cb(first)
+ limit--
+ if limit <= 0 {
+ return false
+ }
+ }
+ } else {
+ // possible cases:
+ // 1 the offset given skips the first node entirely
+ // 2 the offset skips none or part of the first node, but the limit requires some of the second node.
+ // 3 the offset skips none or part of the first node, and the limit stops our search on the first node.
+ if offset >= first.size {
+ offset -= first.size // 1
+ } else {
+ if first.traverseByOffset(offset, limit, descending, leavesOnly, cb) {
+ return true
+ }
+ // number of leaves which could actually be called from inside
+ delta := first.size - offset
+ offset = 0
+ if delta >= limit {
+ return true // 3
+ }
+ limit -= delta // 2
+ }
+ }
+
+ // because of the caller guarantees and the way we handle the first node,
+ // at this point we know that limit > 0 and there must be some values in
+ // this second node that we include.
+
+ // => if the second node is a leaf, it has to be included.
+ if second.IsLeaf() {
+ return cb(second)
+ }
+ // => if it is not a leaf, it will still be enough to recursively call this
+ // function with the updated offset and limit
+ return second.traverseByOffset(offset, limit, descending, leavesOnly, cb)
+}
+
+// Only used in testing...
+func (node *Node) lmd() *Node {
+ if node.height == 0 {
+ return node
+ }
+ return node.getLeftNode().lmd()
+}
+
+// Only used in testing...
+func (node *Node) rmd() *Node {
+ if node.height == 0 {
+ return node
+ }
+ return node.getRightNode().rmd()
+}
+
+func maxInt8(a, b int8) int8 {
+ if a > b {
+ return a
+ }
+ return b
+}
diff --git a/gnovm/pkg/benchops/gno/avl/tree.gno b/gnovm/pkg/benchops/gno/avl/tree.gno
new file mode 100644
index 00000000000..e7aa55eb7e4
--- /dev/null
+++ b/gnovm/pkg/benchops/gno/avl/tree.gno
@@ -0,0 +1,103 @@
+package avl
+
+type IterCbFn func(key string, value interface{}) bool
+
+//----------------------------------------
+// Tree
+
+// The zero struct can be used as an empty tree.
+type Tree struct {
+ node *Node
+}
+
+// NewTree creates a new empty AVL tree.
+func NewTree() *Tree {
+ return &Tree{
+ node: nil,
+ }
+}
+
+// Size returns the number of key-value pair in the tree.
+func (tree *Tree) Size() int {
+ return tree.node.Size()
+}
+
+// Has checks whether a key exists in the tree.
+// It returns true if the key exists, otherwise false.
+func (tree *Tree) Has(key string) (has bool) {
+ return tree.node.Has(key)
+}
+
+// Get retrieves the value associated with the given key.
+// It returns the value and a boolean indicating whether the key exists.
+func (tree *Tree) Get(key string) (value interface{}, exists bool) {
+ _, value, exists = tree.node.Get(key)
+ return
+}
+
+// GetByIndex retrieves the key-value pair at the specified index in the tree.
+// It returns the key and value at the given index.
+func (tree *Tree) GetByIndex(index int) (key string, value interface{}) {
+ return tree.node.GetByIndex(index)
+}
+
+// Set inserts a key-value pair into the tree.
+// If the key already exists, the value will be updated.
+// It returns a boolean indicating whether the key was newly inserted or updated.
+func (tree *Tree) Set(key string, value interface{}) (updated bool) {
+ newnode, updated := tree.node.Set(key, value)
+ tree.node = newnode
+ return updated
+}
+
+// Remove removes a key-value pair from the tree.
+// It returns the removed value and a boolean indicating whether the key was found and removed.
+func (tree *Tree) Remove(key string) (value interface{}, removed bool) {
+ newnode, _, value, removed := tree.node.Remove(key)
+ tree.node = newnode
+ return value, removed
+}
+
+// Iterate performs an in-order traversal of the tree within the specified key range.
+// It calls the provided callback function for each key-value pair encountered.
+// If the callback returns true, the iteration is stopped.
+func (tree *Tree) Iterate(start, end string, cb IterCbFn) bool {
+ return tree.node.TraverseInRange(start, end, true, true,
+ func(node *Node) bool {
+ return cb(node.Key(), node.Value())
+ },
+ )
+}
+
+// ReverseIterate performs a reverse in-order traversal of the tree within the specified key range.
+// It calls the provided callback function for each key-value pair encountered.
+// If the callback returns true, the iteration is stopped.
+func (tree *Tree) ReverseIterate(start, end string, cb IterCbFn) bool {
+ return tree.node.TraverseInRange(start, end, false, true,
+ func(node *Node) bool {
+ return cb(node.Key(), node.Value())
+ },
+ )
+}
+
+// IterateByOffset performs an in-order traversal of the tree starting from the specified offset.
+// It calls the provided callback function for each key-value pair encountered, up to the specified count.
+// If the callback returns true, the iteration is stopped.
+func (tree *Tree) IterateByOffset(offset int, count int, cb IterCbFn) bool {
+ return tree.node.TraverseByOffset(offset, count, true, true,
+ func(node *Node) bool {
+ return cb(node.Key(), node.Value())
+ },
+ )
+}
+
+// ReverseIterateByOffset performs a reverse in-order traversal of the tree starting from the specified offset.
+// It calls the provided callback function for each key-value pair encountered, up to the specified count.
+// If the callback returns true, the iteration is stopped.
+func (tree *Tree) ReverseIterateByOffset(offset int, count int, cb IterCbFn) bool {
+ return tree.node.TraverseByOffset(offset, count, false, true,
+ func(node *Node) bool {
+ return cb(node.Key(), node.Value())
+ },
+ )
+}
diff --git a/gnovm/pkg/benchops/gno/opcodes/gno.mod b/gnovm/pkg/benchops/gno/opcodes/gno.mod
new file mode 100644
index 00000000000..326364184cd
--- /dev/null
+++ b/gnovm/pkg/benchops/gno/opcodes/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/x/benchmark/opcodes
diff --git a/gnovm/pkg/benchops/gno/opcodes/opcode.gno b/gnovm/pkg/benchops/gno/opcodes/opcode.gno
new file mode 100644
index 00000000000..05a5f88b48d
--- /dev/null
+++ b/gnovm/pkg/benchops/gno/opcodes/opcode.gno
@@ -0,0 +1,1103 @@
+package opcodes
+
+type foo struct {
+ i int
+}
+
+func (f foo) bark() {
+}
+
+type dog interface {
+ bark()
+}
+
+type foofighter struct {
+ f foo
+}
+
+/* func ExprOps()
+OpEval, [(const (2 int))](const-type int){(const (0 int)), (const (1 int))}
+OpEval, [(const (2 int))](const-type int)
+OpEval, (const-type int)
+OpEval, (const (2 int))
+OpArrayType, [(const (2 int))](const-type int)
+OpCompositeLit, [(const (2 int))](const-type int){(const (0 int)), (const (1 int))}
+OpEval, (const (0 int))
+OpEval, (const (1 int))
+OpArrayLit, [(const (2 int))](const-type int){(const (0 int)), (const (1 int))}
+OpDefine, a := [(const (2 int))](const-type int){(const (0 int)), (const (1 int))}
+OpExec, bodyStmt[0/0/1]=a2 := [(const (2 int))](const-type int){(const (0 int)), (const (1 int))}
+OpEval, [(const (2 int))](const-type int){(const (0 int)), (const (1 int))}
+OpEval, [(const (2 int))](const-type int)
+OpEval, (const-type int)
+OpEval, (const (2 int))
+OpArrayType, [(const (2 int))](const-type int)
+OpCompositeLit, [(const (2 int))](const-type int){(const (0 int)), (const (1 int))}
+OpEval, (const (0 int))
+OpEval, (const (1 int))
+OpArrayLit, [(const (2 int))](const-type int){(const (0 int)), (const (1 int))}
+OpDefine, a2 := [(const (2 int))](const-type int){(const (0 int)), (const (1 int))}
+OpExec, bodyStmt[0/0/2]=m := (const (make func(t type{},z ...interface{})( map[int]int)))(map[(const-type int)] (const-type int))
+OpEval, (const (make func(t type{},z ...interface{})( map[int]int)))(map[(const-type int)] (const-type int))
+OpEval, (const (make func(t type{},z ...interface{})( map[int]int)))
+OpEval, map[(const-type int)] (const-type int)
+OpEval, (const-type int)
+OpEval, (const-type int)
+OpMapType, (typeval{int} type{})
+OpPreCall, (const (make func(t type{},z ...interface{})( map[int]int)))(map[(const-type int)] (const-type int))
+OpCall, make
+OpCallNativeBody, make
+OpReturn, [FRAME FUNC:make RECV:(undefined) (1 args) 4/1/0/2/2 LASTPKG:gno.land/r/x/benchmark LASTRLM:Realm{Path:"gno.land/r/x/benchmark",Time:3}#707D4A13D8A59C3A9220761016E2B0AF5FFCBC5A]
+OpDefine, m := (const (make func(t type{},z ...interface{})( map[int]int)))(map[(const-type int)] (const-type int))
+OpExec, bodyStmt[0/0/3]=s := [](const-type int){(const (0 int)), (const (1 int)), (const (2 int)), (const (3 int)), (const (4 int)), (const (5 int)), (const (6 int)), (const (7 int)), (const (8 int)), (const (9 int))}
+OpEval, [](const-type int){(const (0 int)), (const (1 int)), (const (2 int)), (const (3 int)), (const (4 int)), (const (5 int)), (const (6 int)), (const (7 int)), (const (8 int)), (const (9 int))}
+OpEval, [](const-type int)
+OpEval, (const-type int)
+OpSliceType, [](const-type int)
+OpCompositeLit, [](const-type int){(const (0 int)), (const (1 int)), (const (2 int)), (const (3 int)), (const (4 int)), (const (5 int)), (const (6 int)), (const (7 int)), (const (8 int)), (const (9 int))}
+OpEval, (const (0 int))
+OpEval, (const (1 int))
+OpEval, (const (2 int))
+OpEval, (const (3 int))
+OpEval, (const (4 int))
+OpEval, (const (5 int))
+OpEval, (const (6 int))
+OpEval, (const (7 int))
+OpEval, (const (8 int))
+OpEval, (const (9 int))
+OpSliceLit, [](const-type int){(const (0 int)), (const (1 int)), (const (2 int)), (const (3 int)), (const (4 int)), (const (5 int)), (const (6 int)), (const (7 int)), (const (8 int)), (const (9 int))}
+OpDefine, s := [](const-type int){(const (0 int)), (const (1 int)), (const (2 int)), (const (3 int)), (const (4 int)), (const (5 int)), (const (6 int)), (const (7 int)), (const (8 int)), (const (9 int))}
+OpExec, bodyStmt[0/0/4]=s2 := [](const-type int){(const (9 int)): (const (90 int))}
+OpEval, [](const-type int){(const (9 int)): (const (90 int))}
+OpEval, [](const-type int)
+OpEval, (const-type int)
+OpSliceType, [](const-type int)
+OpCompositeLit, [](const-type int){(const (9 int)): (const (90 int))}
+OpEval, (const (9 int))
+OpEval, (const (90 int))
+OpSliceLit2, [](const-type int){(const (9 int)): (const (90 int))}
+OpDefine, s2 := [](const-type int){(const (9 int)): (const (90 int))}
+OpExec, bodyStmt[0/0/5]=f := foo{i: (const (1 int))}
+OpEval, foo{i: (const (1 int))}
+OpEval, foo
+OpCompositeLit, foo{i: (const (1 int))}
+OpEval, (const (1 int))
+OpStructLit, foo{i: (const (1 int))}
+OpDefine, f := foo{i: (const (1 int))}
+OpExec, bodyStmt[0/0/6]=ff := foofighter{f: f}
+OpEval, foofighter{f: f}
+OpEval, foofighter
+OpCompositeLit, foofighter{f: f}
+OpEval, f
+OpStructLit, foofighter{f: f}
+OpDefine, ff := foofighter{f: f}
+OpExec, bodyStmt[0/0/7]=b := a[(const (0 int))]
+OpEval, a[(const (0 int))]
+OpEval, a
+OpEval, (const (0 int))
+OpIndex1, (array[(0 int),(1 int)] [2]int)
+OpDefine, b := a[(const (0 int))]
+OpExec, bodyStmt[0/0/8]=b, _ = m[(const (0 int))]
+OpEval, m[(const (0 int))]
+OpEval, m
+OpEval, (const (0 int))
+OpIndex2, (map{} map[int]int)
+OpAssgin, b, _ = m[(const (0 int))]
+OpExec, bodyStmt[0/0/9]=b = f.i
+OpEval, f.i
+OpEval, f
+OpSelector, f.i
+OpAssgin, b = f.i
+OpExec, bodyStmt[0/0/10]=subs := s[(const (1 int)):(const (5 int)):(const (10 int))]
+OpEval, s[(const (1 int)):(const (5 int)):(const (10 int))]
+OpEval, s
+OpEval, (const (1 int))
+OpEval, (const (5 int))
+OpEval, (const (10 int))
+OpSlice, s[(const (1 int)):(const (5 int)):(const (10 int))]
+OpDefine, subs := s[(const (1 int)):(const (5 int)):(const (10 int))]
+OpExec, bodyStmt[0/0/11]=ptr := &(a2[(const (0 int))])
+OpEval, &(a2[(const (0 int))])
+OpEval, a2
+OpEval, (const (0 int))
+OpRef, &(a2