From c776e32b08430487083de8ca2ac34c4e435878db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 4 Nov 2024 17:22:46 +0100 Subject: [PATCH] feat: support metadata for genesis txs (#2941) ## Description This PR introduces metadata support for genesis transactions (such as timestamps), in the form of a new Gno genesis state that's easily generate-able. Shoutout to @clockworkgr for sanity checking the idea, and providing insights that ultimately led to this PR materializing. **BREAKING CHANGE** The `GnoGenesisState` is now modified, and all functionality that references it (ex. `gnogenesis`, `tx-archive`) will need to adapt. ### What we wanted to accomplish The Portal Loop does not save "time" information upon restarting (from block 0). This means that any transaction that resulted in a Realm / Package calling `time.Now()` will get differing results when these same transactions are "replayed" as part of the loop (in the genesis state). We wanted to somehow preserve this timestamp information when the transactions (from a previous loop), are executed as part of the genesis building process. For example: - Portal Loop chain is on block 100 - tx A results in a call to `time.Now()`, which returns time T, and saves it somewhere (Realm state) - the Portal Loop restarts, and uses all the transactions it encountered in the past iteration as genesis txs - the genesis is generated by executing the transactions - when tx A is re-executed (this time as part of the genesis block), **it should return time T, as with the original execution context** It is worth noting that this functionality is something we want in `gnodev`, so simple helpers on the Realms to update the timestamps wouldn't work. ### What this PR does I've tried to follow a couple of principles when working on this PR: - don't break backwards compatibility - don't modify critical APIs such as the SDK ABCI, but preserve existing, working, functionality in its original form - don't add another layer to the complexity pancake - don't implement a solution that looks (and works) like a hack I'm not a huge fan of execution hooks, so the solution proposed in this PR doesn't rely on any hook mechanism. Not going with the hook approach also significantly decreases the complexity and preserves readability. The base of this solution is enabling execution context modification, with minimal / no API changes. Having functionality like this, we can tailor the context during critical segments such as genesis generation, and we're not just limited to timestamps (which is the primary use-case). We also introduce a new type of `AppState`, called `MetadataGenesisState`, where metadata is associated with the transactions. We hide the actual `AppState` implementation behind an interface, so existing tools and flows don't break, and work as normal. ### What this PR doesn't do There is more work to be done if this PR is merged: - we need to add support to `tx-archive` for supporting exporting txs with metadata. Should be straightforward to do - the portal loop also needs to be restarted with this new "mode" enabled - we need to add support to existing `gnoland genesis` commands to support the new `MetadataGenesisState`. It is also straightforward, but definitely a bit of work - if we want support for something like this in gnodev, the export / import code of gnodev also needs to be modified to support the new genesis state type (cc @gfanton) - https://github.com/gnolang/gno/pull/2943 Related PRs and issues: - #2751 - #2744 cc @moul @thehowl @jeronimoalbi @ilgooz
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/setup_node.go | 10 +- contribs/gnodev/cmd/gnodev/txs.go | 23 -- contribs/gnodev/pkg/dev/node.go | 26 ++- contribs/gnodev/pkg/dev/node_state.go | 5 +- contribs/gnodev/pkg/dev/packages.go | 24 +- contribs/gnogenesis/internal/txs/txs.go | 9 +- .../internal/txs/txs_add_packages.go | 2 +- .../internal/txs/txs_add_packages_test.go | 4 +- .../gnogenesis/internal/txs/txs_add_sheet.go | 21 +- .../internal/txs/txs_add_sheet_test.go | 33 +-- .../internal/txs/txs_export_test.go | 5 +- .../gnogenesis/internal/txs/txs_list_test.go | 2 +- .../gnogenesis/internal/txs/txs_remove.go | 2 +- .../internal/txs/txs_remove_test.go | 4 +- contribs/gnogenesis/internal/verify/verify.go | 2 +- .../gnogenesis/internal/verify/verify_test.go | 7 +- gno.land/genesis/genesis_txs.jsonl | 34 +-- gno.land/pkg/gnoland/app.go | 27 ++- gno.land/pkg/gnoland/app_test.go | 205 +++++++++++++++++- gno.land/pkg/gnoland/balance_test.go | 23 +- gno.land/pkg/gnoland/genesis.go | 15 +- gno.land/pkg/gnoland/node_inmemory.go | 3 +- gno.land/pkg/gnoland/package.go | 2 + gno.land/pkg/gnoland/types.go | 62 +++++- gno.land/pkg/gnoland/types_test.go | 131 +++++++++++ .../pkg/integration/testing_integration.go | 10 +- gno.land/pkg/integration/testing_node.go | 10 +- tm2/pkg/sdk/baseapp.go | 26 ++- tm2/pkg/sdk/helpers.go | 27 ++- 29 files changed, 581 insertions(+), 173 deletions(-) delete mode 100644 contribs/gnodev/cmd/gnodev/txs.go create mode 100644 gno.land/pkg/gnoland/types_test.go diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index 578cf525751..4b3619b4a7d 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -23,7 +23,7 @@ func setupDevNode( if devCfg.txsFile != "" { // Load txs files var err error - nodeConfig.InitialTxs, err = parseTxs(devCfg.txsFile) + nodeConfig.InitialTxs, err = gnoland.ReadGenesisTxs(ctx, devCfg.txsFile) if err != nil { return nil, fmt.Errorf("unable to load transactions: %w", err) } @@ -35,7 +35,13 @@ func setupDevNode( // Override balances and txs nodeConfig.BalancesList = state.Balances - nodeConfig.InitialTxs = state.Txs + + stateTxs := state.Txs + nodeConfig.InitialTxs = make([]gnoland.TxWithMetadata, len(stateTxs)) + + for index, nodeTx := range stateTxs { + nodeConfig.InitialTxs[index] = nodeTx + } logger.Info("genesis file loaded", "path", devCfg.genesisFile, "txs", len(nodeConfig.InitialTxs)) } diff --git a/contribs/gnodev/cmd/gnodev/txs.go b/contribs/gnodev/cmd/gnodev/txs.go deleted file mode 100644 index 0be33b68702..00000000000 --- a/contribs/gnodev/cmd/gnodev/txs.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - - "github.com/gnolang/gno/tm2/pkg/std" -) - -func parseTxs(txFile string) ([]std.Tx, error) { - if txFile == "" { - return nil, nil - } - - file, loadErr := os.Open(txFile) - if loadErr != nil { - return nil, fmt.Errorf("unable to open tx file %s: %w", txFile, loadErr) - } - defer file.Close() - - return std.ParseTxs(context.Background(), file) -} diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 0e1099eef88..54baa2ea774 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -35,7 +35,7 @@ type NodeConfig struct { BalancesList []gnoland.Balance PackagesPathList []PackagePath Emitter emitter.Emitter - InitialTxs []std.Tx + InitialTxs []gnoland.TxWithMetadata TMConfig *tmcfg.Config SkipFailingGenesisTxs bool NoReplay bool @@ -84,7 +84,7 @@ type Node struct { loadedPackages int // state - initialState, state []std.Tx + initialState, state []gnoland.TxWithMetadata currentStateIndex int } @@ -154,7 +154,7 @@ func (n *Node) GetRemoteAddress() string { // GetBlockTransactions returns the transactions contained // within the specified block, if any -func (n *Node) GetBlockTransactions(blockNum uint64) ([]std.Tx, error) { +func (n *Node) GetBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, error) { n.muNode.RLock() defer n.muNode.RUnlock() @@ -163,21 +163,26 @@ func (n *Node) GetBlockTransactions(blockNum uint64) ([]std.Tx, error) { // GetBlockTransactions returns the transactions contained // within the specified block, if any -func (n *Node) getBlockTransactions(blockNum uint64) ([]std.Tx, error) { +func (n *Node) getBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, error) { int64BlockNum := int64(blockNum) b, err := n.client.Block(&int64BlockNum) if err != nil { - return []std.Tx{}, fmt.Errorf("unable to load block at height %d: %w", blockNum, err) // nothing to see here + return []gnoland.TxWithMetadata{}, fmt.Errorf("unable to load block at height %d: %w", blockNum, err) // nothing to see here } - txs := make([]std.Tx, len(b.Block.Data.Txs)) + txs := make([]gnoland.TxWithMetadata, len(b.Block.Data.Txs)) for i, encodedTx := range b.Block.Data.Txs { var tx std.Tx if unmarshalErr := amino.Unmarshal(encodedTx, &tx); unmarshalErr != nil { return nil, fmt.Errorf("unable to unmarshal amino tx, %w", unmarshalErr) } - txs[i] = tx + txs[i] = gnoland.TxWithMetadata{ + Tx: tx, + Metadata: &gnoland.GnoTxMetadata{ + Timestamp: b.BlockMeta.Header.Time.Unix(), + }, + } } return txs, nil @@ -347,11 +352,14 @@ func (n *Node) SendTransaction(tx *std.Tx) error { return nil } -func (n *Node) getBlockStoreState(ctx context.Context) ([]std.Tx, error) { +func (n *Node) getBlockStoreState(ctx context.Context) ([]gnoland.TxWithMetadata, error) { // get current genesis state genesis := n.GenesisDoc().AppState.(gnoland.GnoGenesisState) - state := genesis.Txs[n.loadedPackages:] // ignore previously loaded packages + initialTxs := genesis.Txs[n.loadedPackages:] // ignore previously loaded packages + + state := append([]gnoland.TxWithMetadata{}, initialTxs...) + lastBlock := n.getLatestBlockNumber() var blocnum uint64 = 1 for ; blocnum <= lastBlock; blocnum++ { diff --git a/contribs/gnodev/pkg/dev/node_state.go b/contribs/gnodev/pkg/dev/node_state.go index 846c4857784..7504580b333 100644 --- a/contribs/gnodev/pkg/dev/node_state.go +++ b/contribs/gnodev/pkg/dev/node_state.go @@ -8,7 +8,6 @@ import ( "github.com/gnolang/gno/contribs/gnodev/pkg/events" "github.com/gnolang/gno/gno.land/pkg/gnoland" bft "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/std" ) var ErrEmptyState = errors.New("empty state") @@ -29,7 +28,7 @@ func (n *Node) SaveCurrentState(ctx context.Context) error { } // Export the current state as list of txs -func (n *Node) ExportCurrentState(ctx context.Context) ([]std.Tx, error) { +func (n *Node) ExportCurrentState(ctx context.Context) ([]gnoland.TxWithMetadata, error) { n.muNode.RLock() defer n.muNode.RUnlock() @@ -42,7 +41,7 @@ func (n *Node) ExportCurrentState(ctx context.Context) ([]std.Tx, error) { return state[:n.currentStateIndex], nil } -func (n *Node) getState(ctx context.Context) ([]std.Tx, error) { +func (n *Node) getState(ctx context.Context) ([]gnoland.TxWithMetadata, error) { if n.state == nil { var err error n.state, err = n.getBlockStoreState(ctx) diff --git a/contribs/gnodev/pkg/dev/packages.go b/contribs/gnodev/pkg/dev/packages.go index 7b560c21e09..7ee628ce39e 100644 --- a/contribs/gnodev/pkg/dev/packages.go +++ b/contribs/gnodev/pkg/dev/packages.go @@ -7,6 +7,7 @@ import ( "path/filepath" "github.com/gnolang/gno/contribs/gnodev/pkg/address" + "github.com/gnolang/gno/gno.land/pkg/gnoland" vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/pkg/gnomod" @@ -118,7 +119,7 @@ func (pm PackagesMap) toList() gnomod.PkgList { return list } -func (pm PackagesMap) Load(fee std.Fee) ([]std.Tx, error) { +func (pm PackagesMap) Load(fee std.Fee) ([]gnoland.TxWithMetadata, error) { pkgs := pm.toList() sorted, err := pkgs.Sort() @@ -127,7 +128,8 @@ func (pm PackagesMap) Load(fee std.Fee) ([]std.Tx, error) { } nonDraft := sorted.GetNonDraftPkgs() - txs := []std.Tx{} + txs := make([]gnoland.TxWithMetadata, 0, len(nonDraft)) + for _, modPkg := range nonDraft { pkg := pm[modPkg.Dir] if pkg.Creator.IsZero() { @@ -141,18 +143,20 @@ func (pm PackagesMap) Load(fee std.Fee) ([]std.Tx, error) { } // Create transaction - tx := std.Tx{ - Fee: fee, - Msgs: []std.Msg{ - vmm.MsgAddPackage{ - Creator: pkg.Creator, - Deposit: pkg.Deposit, - Package: memPkg, + tx := gnoland.TxWithMetadata{ + Tx: std.Tx{ + Fee: fee, + Msgs: []std.Msg{ + vmm.MsgAddPackage{ + Creator: pkg.Creator, + Deposit: pkg.Deposit, + Package: memPkg, + }, }, }, } - tx.Signatures = make([]std.Signature, len(tx.GetSigners())) + tx.Tx.Signatures = make([]std.Signature, len(tx.Tx.GetSigners())) txs = append(txs, tx) } diff --git a/contribs/gnogenesis/internal/txs/txs.go b/contribs/gnogenesis/internal/txs/txs.go index f8a14eafefc..fbf4c6ea3c7 100644 --- a/contribs/gnogenesis/internal/txs/txs.go +++ b/contribs/gnogenesis/internal/txs/txs.go @@ -8,7 +8,6 @@ import ( "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 { @@ -47,7 +46,7 @@ func (c *txsCfg) RegisterFlags(fs *flag.FlagSet) { } // appendGenesisTxs saves the given transactions to the genesis doc -func appendGenesisTxs(genesis *types.GenesisDoc, txs []std.Tx) error { +func appendGenesisTxs(genesis *types.GenesisDoc, txs []gnoland.TxWithMetadata) error { // Initialize the app state if it's not present if genesis.AppState == nil { genesis.AppState = gnoland.GnoGenesisState{} @@ -77,7 +76,7 @@ func appendGenesisTxs(genesis *types.GenesisDoc, txs []std.Tx) error { } // txStore is a wrapper for TM2 transactions -type txStore []std.Tx +type txStore []gnoland.TxWithMetadata // leftMerge merges the two tx stores, with // preference to the left @@ -86,7 +85,7 @@ func (i *txStore) leftMerge(b txStore) error { txHashMap := make(map[string]struct{}, len(*i)) for _, tx := range *i { - txHash, err := getTxHash(tx) + txHash, err := getTxHash(tx.Tx) if err != nil { return err } @@ -95,7 +94,7 @@ func (i *txStore) leftMerge(b txStore) error { } for _, tx := range b { - txHash, err := getTxHash(tx) + txHash, err := getTxHash(tx.Tx) if err != nil { return err } diff --git a/contribs/gnogenesis/internal/txs/txs_add_packages.go b/contribs/gnogenesis/internal/txs/txs_add_packages.go index b07adc777a7..1b4e6e7cffb 100644 --- a/contribs/gnogenesis/internal/txs/txs_add_packages.go +++ b/contribs/gnogenesis/internal/txs/txs_add_packages.go @@ -53,7 +53,7 @@ func execTxsAddPackages( return errInvalidPackageDir } - parsedTxs := make([]std.Tx, 0) + parsedTxs := make([]gnoland.TxWithMetadata, 0) for _, path := range args { // Generate transactions from the packages (recursively) txs, err := gnoland.LoadPackagesFromDir(path, genesisDeployAddress, genesisDeployFee) diff --git a/contribs/gnogenesis/internal/txs/txs_add_packages_test.go b/contribs/gnogenesis/internal/txs/txs_add_packages_test.go index c814ccde957..12a9287f171 100644 --- a/contribs/gnogenesis/internal/txs/txs_add_packages_test.go +++ b/contribs/gnogenesis/internal/txs/txs_add_packages_test.go @@ -118,9 +118,9 @@ func TestGenesis_Txs_Add_Packages(t *testing.T) { state := updatedGenesis.AppState.(gnoland.GnoGenesisState) require.Equal(t, 1, len(state.Txs)) - require.Equal(t, 1, len(state.Txs[0].Msgs)) + require.Equal(t, 1, len(state.Txs[0].Tx.Msgs)) - msgAddPkg, ok := state.Txs[0].Msgs[0].(vmm.MsgAddPackage) + msgAddPkg, ok := state.Txs[0].Tx.Msgs[0].(vmm.MsgAddPackage) require.True(t, ok) assert.Equal(t, packagePath, msgAddPkg.Package.Path) diff --git a/contribs/gnogenesis/internal/txs/txs_add_sheet.go b/contribs/gnogenesis/internal/txs/txs_add_sheet.go index 88673bc29bd..0bbd4b578cc 100644 --- a/contribs/gnogenesis/internal/txs/txs_add_sheet.go +++ b/contribs/gnogenesis/internal/txs/txs_add_sheet.go @@ -4,17 +4,13 @@ import ( "context" "errors" "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" - "github.com/gnolang/gno/tm2/pkg/std" ) -var ( - errInvalidTxsFile = errors.New("unable to open transactions file") - errNoTxsFileSpecified = errors.New("no txs file specified") -) +var errNoTxsFileSpecified = errors.New("no txs file specified") // newTxsAddSheetCmd creates the genesis txs add sheet subcommand func newTxsAddSheetCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { @@ -49,22 +45,13 @@ func execTxsAddSheet( return errNoTxsFileSpecified } - parsedTxs := make([]std.Tx, 0) + parsedTxs := make([]gnoland.TxWithMetadata, 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) + txs, err := gnoland.ReadGenesisTxs(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...) } diff --git a/contribs/gnogenesis/internal/txs/txs_add_sheet_test.go b/contribs/gnogenesis/internal/txs/txs_add_sheet_test.go index a174905c237..6da3faea6ed 100644 --- a/contribs/gnogenesis/internal/txs/txs_add_sheet_test.go +++ b/contribs/gnogenesis/internal/txs/txs_add_sheet_test.go @@ -21,25 +21,27 @@ import ( ) // generateDummyTxs generates dummy transactions -func generateDummyTxs(t *testing.T, count int) []std.Tx { +func generateDummyTxs(t *testing.T, count int) []gnoland.TxWithMetadata { t.Helper() - txs := make([]std.Tx, count) + txs := make([]gnoland.TxWithMetadata, 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)), + txs[i] = gnoland.TxWithMetadata{ + Tx: 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), }, - Fee: std.Fee{ - GasWanted: 1, - GasFee: std.NewCoin(ugnot.Denom, 1000000), - }, - Memo: fmt.Sprintf("tx %d", i), } } @@ -47,7 +49,7 @@ func generateDummyTxs(t *testing.T, count int) []std.Tx { } // encodeDummyTxs encodes the transactions into amino JSON -func encodeDummyTxs(t *testing.T, txs []std.Tx) []string { +func encodeDummyTxs(t *testing.T, txs []gnoland.TxWithMetadata) []string { t.Helper() encodedTxs := make([]string, 0, len(txs)) @@ -104,8 +106,7 @@ func TestGenesis_Txs_Add_Sheets(t *testing.T) { } // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errInvalidTxsFile.Error()) + assert.Error(t, cmd.ParseAndRun(context.Background(), args)) }) t.Run("no txs file", func(t *testing.T) { diff --git a/contribs/gnogenesis/internal/txs/txs_export_test.go b/contribs/gnogenesis/internal/txs/txs_export_test.go index 47fc594d2ec..ad738cd95f7 100644 --- a/contribs/gnogenesis/internal/txs/txs_export_test.go +++ b/contribs/gnogenesis/internal/txs/txs_export_test.go @@ -9,7 +9,6 @@ import ( "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" @@ -117,9 +116,9 @@ func TestGenesis_Txs_Export(t *testing.T) { // Validate the transactions were written down scanner := bufio.NewScanner(outputFile) - outputTxs := make([]std.Tx, 0) + outputTxs := make([]gnoland.TxWithMetadata, 0) for scanner.Scan() { - var tx std.Tx + var tx gnoland.TxWithMetadata require.NoError(t, amino.UnmarshalJSON(scanner.Bytes(), &tx)) diff --git a/contribs/gnogenesis/internal/txs/txs_list_test.go b/contribs/gnogenesis/internal/txs/txs_list_test.go index 5129533dc8f..d4d9f4d7ba8 100644 --- a/contribs/gnogenesis/internal/txs/txs_list_test.go +++ b/contribs/gnogenesis/internal/txs/txs_list_test.go @@ -63,6 +63,6 @@ func TestGenesis_List_All(t *testing.T) { cmdErr := cmd.ParseAndRun(context.Background(), args) require.NoError(t, cmdErr) - require.Len(t, buf.String(), 4442) + require.Len(t, buf.String(), 5262) }) } diff --git a/contribs/gnogenesis/internal/txs/txs_remove.go b/contribs/gnogenesis/internal/txs/txs_remove.go index f767e19bc90..dbfc90fb1dc 100644 --- a/contribs/gnogenesis/internal/txs/txs_remove.go +++ b/contribs/gnogenesis/internal/txs/txs_remove.go @@ -59,7 +59,7 @@ func execTxsRemove(cfg *txsCfg, io commands.IO, args []string) error { for indx, tx := range state.Txs { // Find the hash of the transaction - hash, err := getTxHash(tx) + hash, err := getTxHash(tx.Tx) if err != nil { return fmt.Errorf("unable to generate tx hash, %w", err) } diff --git a/contribs/gnogenesis/internal/txs/txs_remove_test.go b/contribs/gnogenesis/internal/txs/txs_remove_test.go index c031e0d342e..16edbb83e3c 100644 --- a/contribs/gnogenesis/internal/txs/txs_remove_test.go +++ b/contribs/gnogenesis/internal/txs/txs_remove_test.go @@ -97,7 +97,7 @@ func TestGenesis_Txs_Remove(t *testing.T) { } require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - txHash, err := getTxHash(txs[0]) + txHash, err := getTxHash(txs[0].Tx) require.NoError(t, err) // Create the command @@ -124,7 +124,7 @@ func TestGenesis_Txs_Remove(t *testing.T) { assert.Len(t, state.Txs, len(txs)-1) for _, tx := range state.Txs { - genesisTxHash, err := getTxHash(tx) + genesisTxHash, err := getTxHash(tx.Tx) require.NoError(t, err) assert.NotEqual(t, txHash, genesisTxHash) diff --git a/contribs/gnogenesis/internal/verify/verify.go b/contribs/gnogenesis/internal/verify/verify.go index 97ad14cb7f6..9022711ce49 100644 --- a/contribs/gnogenesis/internal/verify/verify.go +++ b/contribs/gnogenesis/internal/verify/verify.go @@ -61,7 +61,7 @@ func execVerify(cfg *verifyCfg, io commands.IO) error { // Validate the initial transactions for _, tx := range state.Txs { - if validateErr := tx.ValidateBasic(); validateErr != nil { + if validateErr := tx.Tx.ValidateBasic(); validateErr != nil { return fmt.Errorf("invalid transacton, %w", validateErr) } } diff --git a/contribs/gnogenesis/internal/verify/verify_test.go b/contribs/gnogenesis/internal/verify/verify_test.go index 76009c34c94..130bd5e09bc 100644 --- a/contribs/gnogenesis/internal/verify/verify_test.go +++ b/contribs/gnogenesis/internal/verify/verify_test.go @@ -9,7 +9,6 @@ import ( "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" ) @@ -45,7 +44,7 @@ func TestGenesis_Verify(t *testing.T) { g.AppState = gnoland.GnoGenesisState{ Balances: []gnoland.Balance{}, - Txs: []std.Tx{ + Txs: []gnoland.TxWithMetadata{ {}, }, } @@ -76,7 +75,7 @@ func TestGenesis_Verify(t *testing.T) { Balances: []gnoland.Balance{ {}, }, - Txs: []std.Tx{}, + Txs: []gnoland.TxWithMetadata{}, } require.NoError(t, g.SaveAs(tempFile.Name())) @@ -102,7 +101,7 @@ func TestGenesis_Verify(t *testing.T) { g := getValidTestGenesis() g.AppState = gnoland.GnoGenesisState{ Balances: []gnoland.Balance{}, - Txs: []std.Tx{}, + Txs: []gnoland.TxWithMetadata{}, } require.NoError(t, g.SaveAs(tempFile.Name())) diff --git a/gno.land/genesis/genesis_txs.jsonl b/gno.land/genesis/genesis_txs.jsonl index daf9fbdc5d4..43453dcd2fc 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":"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":""}} +{"tx": {"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":""}} +{"tx": {"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":""}} +{"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":"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":""}} +{"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":["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":""}} +{"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":"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":""}} +{"tx": {"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":""}} +{"tx": {"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":""}} diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index e784f2148aa..ff1b5a88fea 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -12,6 +12,7 @@ import ( "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" @@ -301,9 +302,31 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci } txResponses := make([]abci.ResponseDeliverTx, 0, len(state.Txs)) + // Run genesis txs for _, tx := range state.Txs { - res := cfg.baseApp.Deliver(tx) + 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) + } + } + + res := cfg.baseApp.Deliver(stdTx, ctxFn) if res.IsErr() { ctx.Logger().Error( "Unable to deliver genesis tx", @@ -319,7 +342,7 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci GasUsed: res.GasUsed, }) - cfg.GenesisTxResultHandler(ctx, tx, res) + cfg.GenesisTxResultHandler(ctx, stdTx, res) } return txResponses, nil } diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 87b624280c6..5e4dcb2b449 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -52,16 +52,18 @@ func TestNewAppWithOptions(t *testing.T) { Amount: []std.Coin{{Amount: 1e15, Denom: "ugnot"}}, }, }, - Txs: []std.Tx{ + Txs: []TxWithMetadata{ { - 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 + 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 + }, }, }, }, @@ -206,7 +208,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(), @@ -220,6 +222,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() 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..613fba51c37 100644 --- a/gno.land/pkg/gnoland/genesis.go +++ b/gno.land/pkg/gnoland/genesis.go @@ -60,8 +60,9 @@ func LoadGenesisBalancesFile(path string) ([]Balance, error) { // 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{} +func LoadGenesisTxsFile(path string, chainID string, genesisRemote string) ([]TxWithMetadata, error) { + txs := make([]TxWithMetadata, 0) + txsBz := osm.MustReadFile(path) txsLines := strings.Split(string(txsBz), "\n") for _, txLine := range txsLines { @@ -73,7 +74,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 +87,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 +102,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 diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index f81838e1eb3..9dccbbfac8d 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -16,7 +16,6 @@ import ( "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" ) type InMemoryNodeConfig struct { @@ -44,7 +43,7 @@ func NewDefaultGenesisConfig(chainid string) *bft.GenesisDoc { }, AppState: &GnoGenesisState{ Balances: []Balance{}, - Txs: []std.Tx{}, + Txs: []TxWithMetadata{}, }, } } 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/types.go b/gno.land/pkg/gnoland/types.go index 016f3279dbd..b1bedb541aa 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -1,8 +1,13 @@ package gnoland import ( + "bufio" + "context" "errors" + "fmt" + "os" + "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -20,6 +25,59 @@ func ProtoGnoAccount() std.Account { } type GnoGenesisState struct { - Balances []Balance `json:"balances"` - Txs []std.Tx `json:"txs"` + Balances []Balance `json:"balances"` + Txs []TxWithMetadata `json:"txs"` +} + +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) + ) + + 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 } diff --git a/gno.land/pkg/gnoland/types_test.go b/gno.land/pkg/gnoland/types_test.go new file mode 100644 index 00000000000..b4625d6d7d6 --- /dev/null +++ b/gno.land/pkg/gnoland/types_test.go @@ -0,0 +1,131 @@ +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/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) + } + }) +} diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index d3f55cfadf7..56b298e4541 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -134,7 +134,7 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { // This genesis will be use when node is started. genesis := &gnoland.GnoGenesisState{ Balances: LoadDefaultGenesisBalanceFile(t, gnoRootDir), - Txs: []std.Tx{}, + Txs: []gnoland.TxWithMetadata{}, } // test1 must be created outside of the loop below because it is already included in genesis so @@ -660,13 +660,13 @@ 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) { +func (pl *pkgsLoader) LoadPackages(creator bft.Address, 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([]std.Tx, len(pkgslist)) + txs := make([]gnoland.TxWithMetadata, len(pkgslist)) for i, pkg := range pkgslist { tx, err := gnoland.LoadPackage(pkg, creator, fee, deposit) if err != nil { @@ -693,7 +693,9 @@ func (pl *pkgsLoader) LoadPackages(creator bft.Address, fee std.Fee, deposit std } } - txs[i] = tx + txs[i] = gnoland.TxWithMetadata{ + Tx: tx, + } } return txs, nil diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go index 5e9e2272049..ef7e05d0787 100644 --- a/gno.land/pkg/integration/testing_node.go +++ b/gno.land/pkg/integration/testing_node.go @@ -54,13 +54,13 @@ func TestingInMemoryNode(t TestingTS, logger *slog.Logger, config *gnoland.InMem // TestingNodeConfig constructs an in-memory node configuration // with default packages and genesis transactions already loaded. // It will return the default creator address of the loaded packages. -func TestingNodeConfig(t TestingTS, gnoroot string, additionalTxs ...std.Tx) (*gnoland.InMemoryNodeConfig, bft.Address) { +func TestingNodeConfig(t TestingTS, gnoroot string, additionalTxs ...gnoland.TxWithMetadata) (*gnoland.InMemoryNodeConfig, bft.Address) { cfg := TestingMinimalNodeConfig(t, gnoroot) creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 balances := LoadDefaultGenesisBalanceFile(t, gnoroot) - txs := []std.Tx{} + txs := make([]gnoland.TxWithMetadata, 0) txs = append(txs, LoadDefaultPackages(t, creator, gnoroot)...) txs = append(txs, additionalTxs...) @@ -121,13 +121,13 @@ func DefaultTestingGenesisConfig(t TestingTS, gnoroot string, self crypto.PubKey Amount: std.MustParseCoins(ugnot.ValueString(10000000000000)), }, }, - Txs: []std.Tx{}, + Txs: []gnoland.TxWithMetadata{}, }, } } // 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 { +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))) @@ -148,7 +148,7 @@ func LoadDefaultGenesisBalanceFile(t TestingTS, gnoroot string) []gnoland.Balanc } // LoadDefaultGenesisTXsFile loads the default genesis transactions file for testing. -func LoadDefaultGenesisTXsFile(t TestingTS, chainid string, gnoroot string) []std.Tx { +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 diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index 3cabc9df336..c11f81d852a 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -336,6 +336,7 @@ func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitC app.deliverState.ctx = app.deliverState.ctx. WithBlockGasMeter(store.NewInfiniteGasMeter()) + // Run the set chain initializer res = app.initChainer(app.deliverState.ctx, req) // sanity check @@ -557,7 +558,9 @@ func (app *BaseApp) CheckTx(req abci.RequestCheckTx) (res abci.ResponseCheckTx) res.Error = ABCIError(std.ErrTxDecode(err.Error())) return } else { - result := app.runTx(RunTxModeCheck, req.Tx, tx) + ctx := app.getContextForTx(RunTxModeCheck, req.Tx) + + result := app.runTx(ctx, tx) res.ResponseBase = result.ResponseBase res.GasWanted = result.GasWanted res.GasUsed = result.GasUsed @@ -573,7 +576,9 @@ func (app *BaseApp) DeliverTx(req abci.RequestDeliverTx) (res abci.ResponseDeliv res.Error = ABCIError(std.ErrTxDecode(err.Error())) return } else { - result := app.runTx(RunTxModeDeliver, req.Tx, tx) + ctx := app.getContextForTx(RunTxModeDeliver, req.Tx) + + result := app.runTx(ctx, tx) res.ResponseBase = result.ResponseBase res.GasWanted = result.GasWanted res.GasUsed = result.GasUsed @@ -701,14 +706,17 @@ func (app *BaseApp) cacheTxContext(ctx Context) (Context, store.MultiStore) { // anteHandler. The provided txBytes may be nil in some cases, eg. in tests. For // further details on transaction execution, reference the BaseApp SDK // documentation. -func (app *BaseApp) runTx(mode RunTxMode, txBytes []byte, tx Tx) (result Result) { - // NOTE: GasWanted should be returned by the AnteHandler. GasUsed is - // determined by the GasMeter. We need access to the context to get the gas - // meter so we initialize upfront. - var gasWanted int64 +func (app *BaseApp) runTx(ctx Context, tx Tx) (result Result) { + var ( + // NOTE: GasWanted should be returned by the AnteHandler. GasUsed is + // determined by the GasMeter. We need access to the context to get the gas + // meter so we initialize upfront. + gasWanted int64 + + ms = ctx.MultiStore() + mode = ctx.Mode() + ) - ctx := app.getContextForTx(mode, txBytes) - ms := ctx.MultiStore() if mode == RunTxModeDeliver { gasleft := ctx.BlockGasMeter().Remaining() ctx = ctx.WithGasMeter(store.NewPassthroughGasMeter( diff --git a/tm2/pkg/sdk/helpers.go b/tm2/pkg/sdk/helpers.go index d61e8115671..ea5cdfea2da 100644 --- a/tm2/pkg/sdk/helpers.go +++ b/tm2/pkg/sdk/helpers.go @@ -10,17 +10,36 @@ import ( var isAlphaNumeric = regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString func (app *BaseApp) Check(tx Tx) (result Result) { - return app.runTx(RunTxModeCheck, nil, tx) + ctx := app.getContextForTx(RunTxModeCheck, nil) + + return app.runTx(ctx, tx) } func (app *BaseApp) Simulate(txBytes []byte, tx Tx) (result Result) { - return app.runTx(RunTxModeSimulate, txBytes, tx) + ctx := app.getContextForTx(RunTxModeSimulate, txBytes) + + return app.runTx(ctx, tx) } -func (app *BaseApp) Deliver(tx Tx) (result Result) { - return app.runTx(RunTxModeDeliver, nil, tx) +func (app *BaseApp) Deliver(tx Tx, ctxFns ...ContextFn) (result Result) { + ctx := app.getContextForTx(RunTxModeDeliver, nil) + + for _, ctxFn := range ctxFns { + if ctxFn == nil { + continue + } + + ctx = ctxFn(ctx) + } + + return app.runTx(ctx, tx) } +// ContextFn is the custom execution context builder. +// It can be used to add custom metadata when replaying transactions +// during InitChainer or in the context of a unit test. +type ContextFn func(ctx Context) Context + // Context with current {check, deliver}State of the app // used by tests func (app *BaseApp) NewContext(mode RunTxMode, header abci.Header) Context {