From 8ace17c72eb8b4aae570a4969e765c457d5987e3 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Sat, 13 Apr 2024 21:29:03 +0200 Subject: [PATCH 01/23] feat(gnodev): add balances handler Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/main.go | 279 ++++++++++++++++++---- contribs/gnodev/pkg/dev/keybase.go | 102 ++++++++ contribs/gnodev/pkg/dev/node.go | 85 ++++--- contribs/gnodev/pkg/dev/packages.go | 169 +++++++++++++ contribs/gnodev/pkg/dev/packages_test.go | 71 ++++++ contribs/gnodev/pkg/dev/pkgs.go | 82 ------- gno.land/cmd/genesis/balances_add.go | 76 +----- gno.land/cmd/genesis/balances_add_test.go | 131 ---------- gno.land/cmd/genesis/types.go | 24 -- gno.land/pkg/balances/balances.go | 100 ++++++++ gno.land/pkg/balances/balances_test.go | 190 +++++++++++++++ 11 files changed, 937 insertions(+), 372 deletions(-) create mode 100644 contribs/gnodev/pkg/dev/keybase.go create mode 100644 contribs/gnodev/pkg/dev/packages.go create mode 100644 contribs/gnodev/pkg/dev/packages_test.go delete mode 100644 contribs/gnodev/pkg/dev/pkgs.go create mode 100644 gno.land/pkg/balances/balances.go create mode 100644 gno.land/pkg/balances/balances_test.go diff --git a/contribs/gnodev/main.go b/contribs/gnodev/main.go index 6e6d12fcbdc..9bd52d89ec7 100644 --- a/contribs/gnodev/main.go +++ b/contribs/gnodev/main.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "log/slog" + "math/rand" "net" "net/http" "os" @@ -21,12 +22,19 @@ import ( "github.com/gnolang/gno/contribs/gnodev/pkg/logger" "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" "github.com/gnolang/gno/contribs/gnodev/pkg/watcher" + "github.com/gnolang/gno/gno.land/pkg/balances" + "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/gnoweb" - zaplog "github.com/gnolang/gno/gno.land/pkg/log" + "github.com/gnolang/gno/gno.land/pkg/integration" + gnolog "github.com/gnolang/gno/gno.land/pkg/log" "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/bip39" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" osm "github.com/gnolang/gno/tm2/pkg/os" + "github.com/gnolang/gno/tm2/pkg/std" "github.com/muesli/termenv" ) @@ -37,20 +45,36 @@ const ( EventServerLogName = "Event" ) +var ( + DefaultCreatorName = integration.DefaultAccount_Name + DefaultCreatorAddress = crypto.MustAddressFromString(integration.DefaultAccount_Address) + DefaultCreatorSeed = integration.DefaultAccount_Seed + DefaultFee = std.NewFee(50000, std.MustParseCoin("1000000ugnot")) +) + type devCfg struct { + // Listeners webListenerAddr string nodeRPCListenerAddr string nodeP2PListenerAddr string nodeProxyAppListenerAddr string - minimal bool - verbose bool - hotreload bool - noWatch bool - noReplay bool - maxGas int64 - chainId string - serverMode bool + // Users default + genesisCreator string + home string + root string + + // Node Configuration + minimal bool + verbose bool + hotreload bool + noWatch bool + noReplay bool + maxGas int64 + chainId string + serverMode bool + balancesFile string + additionalUsers string } var defaultDevOptions = &devCfg{ @@ -58,6 +82,9 @@ var defaultDevOptions = &devCfg{ maxGas: 10_000_000_000, webListenerAddr: "127.0.0.1:8888", nodeRPCListenerAddr: "127.0.0.1:36657", + genesisCreator: DefaultCreatorAddress.String(), + home: gnoenv.HomeDir(), + root: gnoenv.RootDir(), // As we have no reason to configure this yet, set this to random port // to avoid potential conflict with other app @@ -87,6 +114,20 @@ additional specified paths.`, } func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.home, + "home", + defaultDevOptions.home, + "user's local directory for keys", + ) + + fs.StringVar( + &c.root, + "root", + defaultDevOptions.root, + "gno root directory", + ) + fs.StringVar( &c.webListenerAddr, "web-listener", @@ -98,14 +139,28 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { &c.nodeRPCListenerAddr, "node-rpc-listener", defaultDevOptions.nodeRPCListenerAddr, - "gnoland rpc node listening address", + "listening address for GnoLand RPC node", + ) + + fs.StringVar( + &c.nodeRPCListenerAddr, + "add-users", + defaultDevOptions.nodeRPCListenerAddr, + "pre-add users, separated by commas", + ) + + fs.StringVar( + &c.genesisCreator, + "creator", + defaultDevOptions.genesisCreator, + "name of the genesis creator (from Keybase) or address", ) fs.BoolVar( &c.minimal, "minimal", defaultDevOptions.minimal, - "do not load packages from examples directory", + "do not load packages from the examples directory", ) fs.BoolVar( @@ -119,7 +174,7 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { &c.verbose, "verbose", defaultDevOptions.verbose, - "verbose output when deving", + "enable verbose output for development", ) fs.StringVar( @@ -133,21 +188,21 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { &c.noWatch, "no-watch", defaultDevOptions.noWatch, - "do not watch for files change", + "do not watch for file changes", ) fs.BoolVar( &c.noReplay, "no-replay", defaultDevOptions.noReplay, - "do not replay previous transactions on reload", + "do not replay previous transactions upon reload", ) fs.Int64Var( &c.maxGas, "max-gas", defaultDevOptions.maxGas, - "set the maximum gas by block", + "set the maximum gas per block", ) } @@ -155,20 +210,6 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { ctx, cancel := context.WithCancelCause(context.Background()) defer cancel(nil) - // Guess root dir - gnoroot := gnoenv.RootDir() - - // Check and Parse packages - pkgpaths, err := parseArgsPackages(args) - if err != nil { - return fmt.Errorf("unable to parse package paths: %w", err) - } - - if !cfg.minimal { - examplesDir := filepath.Join(gnoroot, "examples") - pkgpaths = append(pkgpaths, examplesDir) - } - // Setup Raw Terminal rt, restore, err := setupRawTerm(cfg, io) if err != nil { @@ -186,9 +227,21 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { loggerEvents := logger.WithGroup(EventServerLogName) emitterServer := emitter.NewServer(loggerEvents) + // load keybase + kb, err := setupKeybase(cfg) + if err != nil { + return fmt.Errorf("unable to load keybase: %w", err) + } + + // Check and Parse packages + pkgpaths, err := resolvePackagesPathFromArgs(cfg, kb, args) + if err != nil { + return fmt.Errorf("unable to parse package paths: %w", err) + } + // Setup Dev Node // XXX: find a good way to export or display node logs - devNode, err := setupDevNode(ctx, logger, cfg, emitterServer, pkgpaths) + devNode, err := setupDevNode(ctx, logger, cfg, emitterServer, kb, pkgpaths) if err != nil { return err } @@ -393,14 +446,19 @@ func setupDevNode( logger *slog.Logger, cfg *devCfg, remitter emitter.Emitter, - pkgspath []string, + kb keys.Keybase, + pkgspath []dev.PackagePath, ) (*gnodev.Node, error) { nodeLogger := logger.WithGroup(NodeLogName) - gnoroot := gnoenv.RootDir() + balances, err := generateBalances(kb, cfg) + if err != nil { + return nil, fmt.Errorf("unable to generate balances: %w", err) + } // configure gnoland node - config := gnodev.DefaultNodeConfig(gnoroot) + config := gnodev.DefaultNodeConfig(cfg.root) + config.BalancesList = balances.List() config.PackagesPathList = pkgspath config.TMConfig.RPC.ListenAddress = resolveUnixOrTCPAddr(cfg.nodeRPCListenerAddr) config.NoReplay = cfg.noReplay @@ -425,26 +483,79 @@ func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) htt return app.Router } -func parseArgsPackages(args []string) (paths []string, err error) { - paths = make([]string, len(args)) +func resolvePackagesPathFromArgs(cfg *devCfg, kb keys.Keybase, args []string) ([]dev.PackagePath, error) { + paths := make([]dev.PackagePath, len(args)) + + if cfg.genesisCreator == "" { + return nil, fmt.Errorf("default genesis creator cannot be empty") + } + + defaultKey, err := kb.GetByNameOrAddress(cfg.genesisCreator) + if err != nil { + return nil, fmt.Errorf("unable to get genesis creator %q: %w", cfg.genesisCreator, err) + } + for i, arg := range args { - abspath, err := filepath.Abs(arg) + path, err := dev.ResolvePackagePathQuery(kb, arg) if err != nil { - return nil, fmt.Errorf("invalid path %q: %w", arg, err) + return nil, fmt.Errorf("invalid package path/query %q: %w", arg, err) } - ppath, err := gnomod.FindRootDir(abspath) - if err != nil { - return nil, fmt.Errorf("unable to find root dir of %q: %w", abspath, err) + if path.Creator.IsZero() { + path.Creator = defaultKey.GetAddress() } - paths[i] = ppath + paths[i] = path + } + + // Add examples folder if minimal specified + if !cfg.minimal { + fmt.Println("adding example folder: ", filepath.Join(cfg.root, "examples")) + paths = append(paths, gnodev.PackagePath{ + Path: filepath.Join(cfg.root, "examples"), + Creator: defaultKey.GetAddress(), + Deposit: nil, + }) } return paths, nil } -func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm.KeyPress { +func generateBalances(kb keys.Keybase, cfg *devCfg) (balances.Balances, error) { + bls := balances.New() + amount := std.Coins{std.NewCoin("ugnot", 10e6)} + + keys, err := kb.List() + if err != nil { + return nil, fmt.Errorf("unable to list keys from keybase: %w", err) + } + + // Automatically set every key from keybase to unlimited found + for _, key := range keys { + address := key.GetAddress() + bls[address] = gnoland.Balance{Amount: amount, Address: address} + } + + if cfg.balancesFile == "" { + return bls, nil + } + + file, err := os.Open(cfg.balancesFile) + if err != nil { + return nil, fmt.Errorf("unable to open balance file %q: %w", cfg.balancesFile, err) + } + + blsFile, err := balances.GetBalancesFromSheet(file) + if err != nil { + return nil, fmt.Errorf("unable to read balances file %q: %w", cfg.balancesFile, err) + } + + // Left merge keybase balance into file balance + blsFile.LeftMerge(bls) + return blsFile, nil +} + +func listenForKeyPress(w io.Writer, rt *rawterm.RawTerm) <-chan rawterm.KeyPress { cc := make(chan rawterm.KeyPress, 1) go func() { defer close(cc) @@ -460,6 +571,30 @@ func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm. return cc } +// createAccount creates a new account with the given name and adds it to the keybase. +func createAccount(kb keys.Keybase, accountName string) (keys.Info, error) { + entropy, err := bip39.NewEntropy(256) + if err != nil { + return nil, fmt.Errorf("error creating entropy: %w", err) + } + + mnemonic, err := bip39.NewMnemonic(entropy) + if err != nil { + return nil, fmt.Errorf("error generating mnemonic: %w", err) + } + + return kb.CreateAccount(accountName, mnemonic, "", "", 0, 0) +} + +func checkForError(w io.Writer, err error) { + if err != nil { + fmt.Fprintf(w, "[ERROR] - %s\n", err.Error()) + return + } + + fmt.Fprintln(w, "[DONE]") +} + func resolveUnixOrTCPAddr(in string) (out string) { var err error var addr net.Addr @@ -483,6 +618,64 @@ func resolveUnixOrTCPAddr(in string) (out string) { panic(err) } +func setupKeybase(cfg *devCfg) (keys.Keybase, error) { + kb := keys.NewInMemory() + if cfg.home != "" { + // load home keybase into our inMemory keybase + kbHome, err := keys.NewKeyBaseFromDir(cfg.home) + if err != nil { + return nil, fmt.Errorf("unable to load keybae from dir %q: %w", cfg.home, err) + } + + keys, err := kbHome.List() + if err != nil { + return nil, fmt.Errorf("unable to list keys from keybase %q: %w", cfg.home, err) + } + + for _, key := range keys { + name := key.GetName() + armor, err := kbHome.Export(key.GetName()) + if err != nil { + return nil, fmt.Errorf("unable to export key %q: %w", name, err) + } + + if err := kb.Import(name, armor); err != nil { + return nil, fmt.Errorf("unable to import key %q: %w", name, err) + } + } + } + + // Add additional users to our keybase + addUsers := strings.Split(cfg.additionalUsers, ",") + for _, user := range addUsers { + if _, err := createAccount(kb, user); err != nil { + return nil, fmt.Errorf("unable to create user %q: %w", user, err) + } + } + + // Add default creator it doesn't exist + ok, _ := kb.HasByAddress(DefaultCreatorAddress) + if ok { + return kb, nil + } + + for i := 0; i < 5; i++ { + creatorName := fmt.Sprintf("_testUser%05d\n", rand.Intn(100000)) + ok, _ := kb.HasByName(creatorName) + if ok { + continue + } + + if _, err := kb.CreateAccount(creatorName, DefaultCreatorSeed, "", "", 0, 0); err != nil { + return nil, fmt.Errorf("unable to create default %q account: %w", DefaultCreatorName, err) + } + + return kb, nil + } + + panic("unable to generate ranmdom test user name") +} + func setuplogger(cfg *devCfg, out io.Writer) *slog.Logger { level := slog.LevelInfo if cfg.verbose { @@ -491,7 +684,7 @@ func setuplogger(cfg *devCfg, out io.Writer) *slog.Logger { if cfg.serverMode { zaplogger := logger.NewZapLogger(out, level) - return zaplog.ZapLoggerToSlog(zaplogger) + return gnolog.ZapLoggerToSlog(zaplogger) } // Detect term color profile diff --git a/contribs/gnodev/pkg/dev/keybase.go b/contribs/gnodev/pkg/dev/keybase.go new file mode 100644 index 00000000000..7f69a36c31b --- /dev/null +++ b/contribs/gnodev/pkg/dev/keybase.go @@ -0,0 +1,102 @@ +package dev + +import ( + "fmt" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/crypto/bip39" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/std" +) + +// type Keybase struct { +// keys.Keybase +// } + +// func NewKeybase() *Keybase { +// return &Keybase{ +// Keybase: keys.NewInMemory(), +// } +// } + +// func (to *Keybase) ImportKeybaseFromPath(path string) error { +// from, err := keys.NewKeyBaseFromDir(path) +// if err != nil { +// return fmt.Errorf("unable to load keybase: %w", err) +// } + +// keys, err := from.List() +// if err != nil { +// return fmt.Errorf("unable to list keys: %w", path, err) +// } + +// for _, key := range keys { +// armor, err := from.Export(key.GetName()) +// if err != nil { +// return fmt.Errorf("unable to import key %q: %w", key.GetName(), err) +// } + +// err = to.Import(key.GetName(), armor) +// if err != nil { +// return fmt.Errorf("unable to import key %q: %w", key.GetName(), err) +// } +// } + +// return nil +// } + +// type PackagePath struct { +// Path string +// CreatorNameOrAddress string +// } + +// func ParsePackagePath(path string) (PackagePath, error) { +// var ppath PackagePath + +// upath, err := url.Parse(path) +// if err != nil { +// return ppath, fmt.Errorf("unable to parse package path: %w", err) +// } + +// // Get path +// ppath.Path = filepath.Clean(upath.Path) +// // Check for options +// ppath.CreatorNameOrAddress = upath.Query().Get("creator") +// return ppath, nil +// } + +// func LoadKeyabaseBalanceFromPath(kb keys.Keybase) ([]gnoland.Balance, error) { +// keys, err := kb.List() +// if err != nil { +// return nil, nil +// } + +// for _, info := range keys { +// info.GetName() +// } +// } + +// loadAccount with the given name and adds it to the keybase. +func loadAccount(kb keys.Keybase, accountName string) (gnoland.Balance, error) { + var balance gnoland.Balance + entropy, err := bip39.NewEntropy(256) + if err != nil { + return balance, fmt.Errorf("error creating entropy: %w", err) + } + + mnemonic, err := bip39.NewMnemonic(entropy) + if err != nil { + return balance, fmt.Errorf("error generating mnemonic: %w", err) + } + + var keyInfo keys.Info + if keyInfo, err = kb.CreateAccount(accountName, mnemonic, "", "", 0, 0); err != nil { + return balance, fmt.Errorf("unable to create account: %w", err) + } + + address := keyInfo.GetAddress() + return gnoland.Balance{ + Address: address, + Amount: std.Coins{std.NewCoin("ugnot", 10e6)}, + }, nil +} diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 02d97dff733..baa8c3626df 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "path/filepath" "strings" "unicode" @@ -27,22 +28,36 @@ import ( ) type NodeConfig struct { - PackagesPathList []string - TMConfig *tmcfg.Config - NoReplay bool - MaxGasPerBlock int64 - ChainID string + DefaultCreator crypto.Address + BalancesList []gnoland.Balance + PackagesPathList []PackagePath + TMConfig *tmcfg.Config + SkipFailingGenesisTxs bool + NoReplay bool + MaxGasPerBlock int64 + ChainID string } func DefaultNodeConfig(rootdir string) *NodeConfig { tmc := gnoland.NewDefaultTMConfig(rootdir) tmc.Consensus.SkipTimeoutCommit = false // avoid time drifting, see issue #1507 + defaultCreator := crypto.MustAddressFromString(integration.DefaultAccount_Address) + balances := []gnoland.Balance{ + { + Address: defaultCreator, + Amount: std.Coins{std.NewCoin("ugnot", 10e6)}, + }, + } + return &NodeConfig{ - ChainID: tmc.ChainID(), - PackagesPathList: []string{}, - TMConfig: tmc, - MaxGasPerBlock: 10_000_000_000, + DefaultCreator: defaultCreator, + BalancesList: balances, + ChainID: tmc.ChainID(), + PackagesPathList: []PackagePath{}, + TMConfig: tmc, + SkipFailingGenesisTxs: true, + MaxGasPerBlock: 10_000_000_000, } } @@ -54,30 +69,21 @@ type Node struct { emitter emitter.Emitter client client.Client logger *slog.Logger - pkgs PkgsMap // path -> pkg + pkgs PackagesMap // path -> pkg // keep track of number of loaded package to be able to skip them on restore loadedPackages int } -var ( - DefaultFee = std.NewFee(50000, std.MustParseCoin("1000000ugnot")) - DefaultCreator = crypto.MustAddressFromString(integration.DefaultAccount_Address) - DefaultBalance = []gnoland.Balance{ - { - Address: DefaultCreator, - Amount: std.MustParseCoins("10000000000000ugnot"), - }, - } -) +var DefaultFee = std.NewFee(50000, std.MustParseCoin("1000000ugnot")) func NewDevNode(ctx context.Context, logger *slog.Logger, emitter emitter.Emitter, cfg *NodeConfig) (*Node, error) { - mpkgs, err := newPkgsMap(cfg.PackagesPathList) + mpkgs, err := NewPackagesMap(cfg.PackagesPathList) if err != nil { return nil, fmt.Errorf("unable map pkgs list: %w", err) } - pkgsTxs, err := mpkgs.Load(DefaultCreator, DefaultFee, nil) + pkgsTxs, err := mpkgs.Load(cfg.DefaultCreator, DefaultFee) if err != nil { return nil, fmt.Errorf("unable to load genesis packages: %w", err) } @@ -86,7 +92,7 @@ func NewDevNode(ctx context.Context, logger *slog.Logger, emitter emitter.Emitte // generate genesis state genesis := gnoland.GnoGenesisState{ - Balances: DefaultBalance, + Balances: cfg.BalancesList, Txs: pkgsTxs, } @@ -131,15 +137,36 @@ func (d *Node) GetRemoteAddress() string { func (d *Node) UpdatePackages(paths ...string) error { var n int for _, path := range paths { + abspath, err := filepath.Abs(path) + if err != nil { + return fmt.Errorf("unable to resolve abs path of %q: %w", path, err) + } + + creator := d.config.DefaultCreator + var deposit std.Coins + for _, ppath := range d.config.PackagesPathList { + if !strings.HasPrefix(abspath, ppath.Path) { + continue + } + + creator = ppath.Creator + deposit = ppath.Deposit + } + // List all packages from target path - pkgslist, err := gnomod.ListPkgs(path) + pkgslist, err := gnomod.ListPkgs(abspath) if err != nil { return fmt.Errorf("failed to list gno packages for %q: %w", path, err) } // Update or add package in the current known list. for _, pkg := range pkgslist { - d.pkgs[pkg.Dir] = pkg + d.pkgs[pkg.Dir] = Package{ + Pkg: pkg, + Creator: creator, + Deposit: deposit, + } + d.logger.Debug("pkgs update", "name", pkg.Name, "path", pkg.Dir) } @@ -159,13 +186,13 @@ func (d *Node) Reset(ctx context.Context) error { } // Generate a new genesis state based on the current packages - txs, err := d.pkgs.Load(DefaultCreator, DefaultFee, nil) + txs, err := d.pkgs.Load(d.config.DefaultCreator, DefaultFee) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } genesis := gnoland.GnoGenesisState{ - Balances: DefaultBalance, + Balances: d.config.BalancesList, Txs: txs, } @@ -217,14 +244,14 @@ func (d *Node) Reload(ctx context.Context) error { } // Load genesis packages - pkgsTxs, err := d.pkgs.Load(DefaultCreator, DefaultFee, nil) + pkgsTxs, err := d.pkgs.Load(d.config.DefaultCreator, DefaultFee) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } // Create genesis with loaded pkgs + previous state genesis := gnoland.GnoGenesisState{ - Balances: DefaultBalance, + Balances: d.config.BalancesList, Txs: append(pkgsTxs, state...), } diff --git a/contribs/gnodev/pkg/dev/packages.go b/contribs/gnodev/pkg/dev/packages.go new file mode 100644 index 00000000000..65acb8ec585 --- /dev/null +++ b/contribs/gnodev/pkg/dev/packages.go @@ -0,0 +1,169 @@ +package dev + +import ( + "errors" + "fmt" + "net/url" + "path/filepath" + + 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" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type PackagePath struct { + Path string + Creator crypto.Address + Deposit std.Coins +} + +func ResolvePackagePathQuery(kb keys.Keybase, path string) (PackagePath, error) { + var ppath PackagePath + + upath, err := url.Parse(filepath.Clean(path)) + if err != nil { + return ppath, fmt.Errorf("malformed path/query: %w", err) + } + ppath.Path = upath.Path + + // Check for creator option + creator := upath.Query().Get("creator") + if creator != "" { + address, err := crypto.AddressFromBech32(creator) + if err != nil { + info, nameErr := kb.GetByName(creator) + if nameErr != nil { + return ppath, fmt.Errorf("invalid name or address for creator %q", creator) + } + + address = info.GetAddress() + } + + ppath.Creator = address + } + + // Check for deposit option + deposit := upath.Query().Get("deposit") + if deposit != "" { + coins, err := std.ParseCoins(deposit) + if err != nil { + return ppath, fmt.Errorf( + "unable to parse deposit amount %q (should be in the form xxxugnot): %w", deposit, err, + ) + } + + ppath.Deposit = coins + } + + return ppath, nil +} + +type Package struct { + gnomod.Pkg + Creator crypto.Address + Deposit std.Coins +} + +type PackagesMap map[string]Package + +var ( + ErrEmptyCreatorPackage = errors.New("no creator specified for package") + ErrEmptyDepositPackage = errors.New("no deposit specified for package") +) + +func NewPackagesMap(ppaths []PackagePath) (PackagesMap, error) { + pkgs := make(map[string]Package) + for _, ppath := range ppaths { + fmt.Println(ppath) + if ppath.Creator.IsZero() { + return nil, fmt.Errorf("unable to load package %q: %w", ppath.Path, ErrEmptyCreatorPackage) + } + + // if ppath.Deposit.Empty() { + // return nil, fmt.Errorf("unable to load package %q: %w", ppath.Path, ErrEmptyDepositPackage) + // } + + abspath, err := filepath.Abs(ppath.Path) + if err != nil { + return nil, fmt.Errorf("unable to guess absolute path for %q: %w", ppath.Path, err) + } + + // rootdir, err := gnomod.FindRootDir(abspath) + // if err != nil { + // return nil, fmt.Errorf("unable to find rootdir for package %q: %w", ppath.Path, err) + // } + + // list all packages from target path + pkgslist, err := gnomod.ListPkgs(abspath) + if err != nil { + return nil, fmt.Errorf("listing gno packages: %w", err) + } + + for _, pkg := range pkgslist { + if pkg.Dir == "" { + continue + } + + if _, ok := pkgs[pkg.Dir]; ok { + continue // skip + } + pkgs[pkg.Dir] = Package{ + Pkg: pkg, + Creator: ppath.Creator, + Deposit: ppath.Deposit, + } + } + } + + return pkgs, nil +} + +func (pm PackagesMap) toList() gnomod.PkgList { + list := make([]gnomod.Pkg, 0, len(pm)) + for _, pkg := range pm { + list = append(list, pkg.Pkg) + } + return list +} + +func (pm PackagesMap) Load(creator bft.Address, fee std.Fee) ([]std.Tx, error) { + pkgs := pm.toList() + + sorted, err := pkgs.Sort() + if err != nil { + return nil, fmt.Errorf("unable to sort pkgs: %w", err) + } + + nonDraft := sorted.GetNonDraftPkgs() + txs := []std.Tx{} + for _, modPkg := range nonDraft { + pkg := pm[modPkg.Dir] + + // Open files in directory as MemPackage. + memPkg := gno.ReadMemPackage(modPkg.Dir, modPkg.Name) + if err := memPkg.Validate(); err != nil { + return nil, fmt.Errorf("invalid package: %w", err) + } + + // Create transaction + 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())) + txs = append(txs, tx) + } + + return txs, nil +} diff --git a/contribs/gnodev/pkg/dev/packages_test.go b/contribs/gnodev/pkg/dev/packages_test.go new file mode 100644 index 00000000000..e0941e29d98 --- /dev/null +++ b/contribs/gnodev/pkg/dev/packages_test.go @@ -0,0 +1,71 @@ +package dev + +import ( + "testing" + + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResolvePackagePathQuery(t *testing.T) { + var ( + testingName = "testAccount" + testingMnemonic = `special hip mail knife manual boy essay certain broccoli group token exchange problem subject garbage chaos program monitor happy magic upgrade kingdom cluster enemy` + testingAddress = crypto.MustAddressFromString("g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na") + ) + + kb := keys.NewInMemory() + kb.CreateAccount(testingName, testingMnemonic, "", "", 0, 0) + + cases := []struct { + Path string + ExpectedPackagePath PackagePath + ShouldFail bool + }{ + {".", PackagePath{ + Path: ".", + }, false}, + {"/simple/path", PackagePath{ + Path: "/simple/path", + }, false}, + {"/ambiguo/u//s/path///", PackagePath{ + Path: "/ambiguo/u/s/path", + }, false}, + {"/path/with/creator?creator=testAccount", PackagePath{ + Path: "/path/with/creator", + Creator: testingAddress, + }, false}, + {"/path/with/deposit?deposit=100ugnot", PackagePath{ + Path: "/path/with/deposit", + Deposit: std.MustParseCoins("100ugnot"), + }, false}, + {".?creator=g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na&deposit=100ugnot", PackagePath{ + Path: ".", + Creator: testingAddress, + Deposit: std.MustParseCoins("100ugnot"), + }, false}, + + // errors cases + {"/invalid/account?creator=UnknownAccount", PackagePath{}, true}, + {"/invalid/address?creator=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", PackagePath{}, true}, + {"/invalid/deposit?deposit=abcd", PackagePath{}, true}, + } + + for _, tc := range cases { + t.Run(tc.Path, func(t *testing.T) { + result, err := ResolvePackagePathQuery(kb, tc.Path) + if tc.ShouldFail { + assert.Error(t, err) + return + } + require.NoError(t, err) + + assert.Equal(t, tc.ExpectedPackagePath.Path, result.Path) + assert.Equal(t, tc.ExpectedPackagePath.Creator, result.Creator) + assert.Equal(t, tc.ExpectedPackagePath.Deposit.String(), result.Deposit.String()) + }) + } +} diff --git a/contribs/gnodev/pkg/dev/pkgs.go b/contribs/gnodev/pkg/dev/pkgs.go deleted file mode 100644 index c02508ff33d..00000000000 --- a/contribs/gnodev/pkg/dev/pkgs.go +++ /dev/null @@ -1,82 +0,0 @@ -package dev - -import ( - "fmt" - - 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" - bft "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/std" -) - -type PkgsMap map[string]gnomod.Pkg - -func newPkgsMap(paths []string) (PkgsMap, error) { - pkgs := make(map[string]gnomod.Pkg) - for _, path := range paths { - // list all packages from target path - pkgslist, err := gnomod.ListPkgs(path) - if err != nil { - return nil, fmt.Errorf("listing gno packages: %w", err) - } - - for _, pkg := range pkgslist { - if pkg.Dir == "" { - continue - } - - if _, ok := pkgs[pkg.Dir]; ok { - continue // skip - } - pkgs[pkg.Dir] = pkg - } - } - - // Filter out draft packages. - return pkgs, nil -} - -func (pm PkgsMap) toList() gnomod.PkgList { - list := make([]gnomod.Pkg, 0, len(pm)) - for _, pkg := range pm { - list = append(list, pkg) - } - return list -} - -func (pm PkgsMap) Load(creator bft.Address, fee std.Fee, deposit std.Coins) ([]std.Tx, error) { - pkgs := pm.toList() - - sorted, err := pkgs.Sort() - if err != nil { - return nil, fmt.Errorf("unable to sort pkgs: %w", err) - } - - nonDraft := sorted.GetNonDraftPkgs() - txs := []std.Tx{} - for _, pkg := range nonDraft { - // Open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) - if err := memPkg.Validate(); err != nil { - return nil, fmt.Errorf("invalid package: %w", err) - } - - // Create transaction - tx := std.Tx{ - Fee: fee, - Msgs: []std.Msg{ - vmm.MsgAddPackage{ - Creator: creator, - Package: memPkg, - Deposit: deposit, - }, - }, - } - - tx.Signatures = make([]std.Signature, len(tx.GetSigners())) - txs = append(txs, tx) - } - - return txs, nil -} diff --git a/gno.land/cmd/genesis/balances_add.go b/gno.land/cmd/genesis/balances_add.go index d1e88efcc6b..46fde3a7867 100644 --- a/gno.land/cmd/genesis/balances_add.go +++ b/gno.land/cmd/genesis/balances_add.go @@ -8,8 +8,8 @@ import ( "fmt" "io" "os" - "strings" + "github.com/gnolang/gno/gno.land/pkg/balances" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/types" @@ -93,16 +93,16 @@ func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io commands.IO) e return errNoBalanceSource } - finalBalances := make(accountBalances) + finalBalances := balances.New() // Get the balance sheet from the source if singleEntriesSet { - balances, err := getBalancesFromEntries(cfg.singleEntries) + balances, err := balances.GetBalancesFromEntries(cfg.singleEntries...) if err != nil { return fmt.Errorf("unable to get balances from entries, %w", err) } - finalBalances.leftMerge(balances) + finalBalances.LeftMerge(balances) } if balanceSheetSet { @@ -112,12 +112,12 @@ func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io commands.IO) e return fmt.Errorf("unable to open balance sheet, %w", loadErr) } - balances, err := getBalancesFromSheet(file) + balances, err := balances.GetBalancesFromSheet(file) if err != nil { return fmt.Errorf("unable to get balances from balance sheet, %w", err) } - finalBalances.leftMerge(balances) + finalBalances.LeftMerge(balances) } if txFileSet { @@ -132,7 +132,7 @@ func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io commands.IO) e return fmt.Errorf("unable to get balances from tx file, %w", err) } - finalBalances.leftMerge(balances) + finalBalances.LeftMerge(balances) } // Initialize genesis app state if it is not initialized already @@ -149,10 +149,10 @@ func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io commands.IO) e // Merge the two balance sheets, with the input // having precedence over the genesis balances - finalBalances.leftMerge(genesisBalances) + finalBalances.LeftMerge(genesisBalances) // Save the balances - state.Balances = finalBalances.toList() + state.Balances = finalBalances.List() genesis.AppState = state // Save the updated genesis @@ -174,56 +174,6 @@ func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io commands.IO) e return nil } -// getBalancesFromEntries extracts the balance entries -// from the array of balance -func getBalancesFromEntries(entries []string) (accountBalances, error) { - balances := make(accountBalances) - - for _, entry := range entries { - var balance gnoland.Balance - if err := balance.Parse(entry); err != nil { - return nil, fmt.Errorf("unable to parse balance entry: %w", err) - } - balances[balance.Address] = balance - } - - return balances, nil -} - -// getBalancesFromSheet extracts the balance sheet from the passed in -// balance sheet file, that has the format of
=ugnot -func getBalancesFromSheet(sheet io.Reader) (accountBalances, error) { - // Parse the balances - balances := make(accountBalances) - scanner := bufio.NewScanner(sheet) - - for scanner.Scan() { - entry := scanner.Text() - - // Remove comments - entry = strings.Split(entry, "#")[0] - entry = strings.TrimSpace(entry) - - // Skip empty lines - if entry == "" { - continue - } - - var balance gnoland.Balance - if err := balance.Parse(entry); err != nil { - return nil, fmt.Errorf("unable to extract balance data, %w", err) - } - - balances[balance.Address] = balance - } - - if err := scanner.Err(); err != nil { - return nil, fmt.Errorf("error encountered while scanning, %w", err) - } - - return balances, nil -} - // getBalancesFromTransactions constructs a balance map based on MsgSend messages. // This way of determining the final balance sheet is not valid, since it doesn't take into // account different message types (ex. MsgCall) that can initialize accounts with some balance values. @@ -234,8 +184,8 @@ func getBalancesFromTransactions( ctx context.Context, io commands.IO, reader io.Reader, -) (accountBalances, error) { - balances := make(accountBalances) +) (balances.Balances, error) { + balances := balances.New() scanner := bufio.NewScanner(reader) @@ -336,9 +286,9 @@ func getBalancesFromTransactions( // mapGenesisBalancesFromState extracts the initial account balances from the // genesis app state -func mapGenesisBalancesFromState(state gnoland.GnoGenesisState) (accountBalances, error) { +func mapGenesisBalancesFromState(state gnoland.GnoGenesisState) (balances.Balances, error) { // Construct the initial genesis balance sheet - genesisBalances := make(accountBalances) + genesisBalances := balances.New() for _, balance := range state.Balances { genesisBalances[balance.Address] = balance diff --git a/gno.land/cmd/genesis/balances_add_test.go b/gno.land/cmd/genesis/balances_add_test.go index 0dd3366869f..edcbbd589d9 100644 --- a/gno.land/cmd/genesis/balances_add_test.go +++ b/gno.land/cmd/genesis/balances_add_test.go @@ -4,8 +4,6 @@ import ( "bytes" "context" "fmt" - "math" - "strconv" "strings" "testing" @@ -408,135 +406,6 @@ func TestGenesis_Balances_Add(t *testing.T) { }) } -func TestBalances_GetBalancesFromEntries(t *testing.T) { - t.Parallel() - - t.Run("valid balances", func(t *testing.T) { - t.Parallel() - - // Generate dummy keys - dummyKeys := getDummyKeys(t, 2) - amount := std.NewCoins(std.NewCoin("ugnot", 10)) - - balances := make([]string, len(dummyKeys)) - - for index, key := range dummyKeys { - balances[index] = fmt.Sprintf( - "%s=%dugnot", - key.Address().String(), - amount.AmountOf("ugnot"), - ) - } - - balanceMap, err := getBalancesFromEntries(balances) - require.NoError(t, err) - - // Validate the balance map - assert.Len(t, balanceMap, len(dummyKeys)) - for _, key := range dummyKeys { - assert.Equal(t, amount, balanceMap[key.Address()].Amount) - } - }) - - t.Run("malformed balance, invalid format", func(t *testing.T) { - t.Parallel() - - balances := []string{ - "malformed balance", - } - - balanceMap, err := getBalancesFromEntries(balances) - - assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, "malformed entry") - }) - - t.Run("malformed balance, invalid address", func(t *testing.T) { - t.Parallel() - - balances := []string{ - "dummyaddress=10ugnot", - } - - balanceMap, err := getBalancesFromEntries(balances) - - assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, "invalid address") - }) - - t.Run("malformed balance, invalid amount", func(t *testing.T) { - t.Parallel() - - dummyKey := getDummyKey(t) - - balances := []string{ - fmt.Sprintf( - "%s=%sugnot", - dummyKey.Address().String(), - strconv.FormatUint(math.MaxUint64, 10), - ), - } - - balanceMap, err := getBalancesFromEntries(balances) - - assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, "invalid amount") - }) -} - -func TestBalances_GetBalancesFromSheet(t *testing.T) { - t.Parallel() - - t.Run("valid balances", func(t *testing.T) { - t.Parallel() - - // Generate dummy keys - dummyKeys := getDummyKeys(t, 2) - amount := std.NewCoins(std.NewCoin("ugnot", 10)) - - balances := make([]string, len(dummyKeys)) - - for index, key := range dummyKeys { - balances[index] = fmt.Sprintf( - "%s=%dugnot", - key.Address().String(), - amount.AmountOf("ugnot"), - ) - } - - reader := strings.NewReader(strings.Join(balances, "\n")) - balanceMap, err := getBalancesFromSheet(reader) - require.NoError(t, err) - - // Validate the balance map - assert.Len(t, balanceMap, len(dummyKeys)) - for _, key := range dummyKeys { - assert.Equal(t, amount, balanceMap[key.Address()].Amount) - } - }) - - t.Run("malformed balance, invalid amount", func(t *testing.T) { - t.Parallel() - - dummyKey := getDummyKey(t) - - balances := []string{ - fmt.Sprintf( - "%s=%sugnot", - dummyKey.Address().String(), - strconv.FormatUint(math.MaxUint64, 10), - ), - } - - reader := strings.NewReader(strings.Join(balances, "\n")) - - balanceMap, err := getBalancesFromSheet(reader) - - assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, "invalid amount") - }) -} - func TestBalances_GetBalancesFromTransactions(t *testing.T) { t.Parallel() diff --git a/gno.land/cmd/genesis/types.go b/gno.land/cmd/genesis/types.go index dba39ea8ec1..a48bfaf7b31 100644 --- a/gno.land/cmd/genesis/types.go +++ b/gno.land/cmd/genesis/types.go @@ -1,8 +1,6 @@ package main import ( - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -37,25 +35,3 @@ func (i *txStore) leftMerge(b txStore) error { return nil } - -type accountBalances map[types.Address]gnoland.Balance // address -> balance (ugnot) - -// toList linearizes the account balances map -func (a accountBalances) toList() []gnoland.Balance { - balances := make([]gnoland.Balance, 0, len(a)) - - for _, balance := range a { - balances = append(balances, balance) - } - - return balances -} - -// leftMerge left-merges the two maps -func (a accountBalances) leftMerge(b accountBalances) { - for key, bVal := range b { - if _, present := (a)[key]; !present { - (a)[key] = bVal - } - } -} diff --git a/gno.land/pkg/balances/balances.go b/gno.land/pkg/balances/balances.go new file mode 100644 index 00000000000..0d6f313b00b --- /dev/null +++ b/gno.land/pkg/balances/balances.go @@ -0,0 +1,100 @@ +package balances + +import ( + "bufio" + "fmt" + "io" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type Balances map[crypto.Address]gnoland.Balance + +func New() Balances { + return make(Balances) +} + +func (balances Balances) Set(address crypto.Address, amount std.Coins) { + balances[address] = gnoland.Balance{ + Address: address, + Amount: amount, + } +} + +func (balances Balances) Get(address crypto.Address) (balance gnoland.Balance, ok bool) { + balance, ok = balances[address] + return +} + +func (balances Balances) List() []gnoland.Balance { + list := make([]gnoland.Balance, 0, len(balances)) + for _, balance := range balances { + list = append(list, balance) + } + return list +} + +// leftMerge left-merges the two maps +func (a Balances) LeftMerge(b Balances) { + for key, bVal := range b { + if _, present := (a)[key]; !present { + (a)[key] = bVal + } + } +} + +func GetBalancesFromEntries(entries ...string) (Balances, error) { + balances := New() + return balances, balances.LoadFromEntries(entries...) +} + +// LoadFromEntries extracts the balance entries in the form of
= +func (balances Balances) LoadFromEntries(entries ...string) error { + for _, entry := range entries { + var balance gnoland.Balance + if err := balance.Parse(entry); err != nil { + return fmt.Errorf("unable to parse balance entry: %w", err) + } + balances[balance.Address] = balance + } + + return nil +} + +func GetBalancesFromSheet(sheet io.Reader) (Balances, error) { + balances := New() + return balances, balances.LoadFromSheet(sheet) +} + +// LoadFromSheet extracts the balance sheet from the passed in +// balance sheet file, that has the format of
=ugnot +func (balances Balances) LoadFromSheet(sheet io.Reader) error { + // Parse the balances + scanner := bufio.NewScanner(sheet) + + for scanner.Scan() { + entry := scanner.Text() + + // Remove comments + entry = strings.Split(entry, "#")[0] + entry = strings.TrimSpace(entry) + + // Skip empty lines + if entry == "" { + continue + } + + if err := balances.LoadFromEntries(entry); err != nil { + return fmt.Errorf("unable to load entries: %w", err) + } + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("error encountered while scanning, %w", err) + } + + return nil +} diff --git a/gno.land/pkg/balances/balances_test.go b/gno.land/pkg/balances/balances_test.go new file mode 100644 index 00000000000..7d42f4ea56c --- /dev/null +++ b/gno.land/pkg/balances/balances_test.go @@ -0,0 +1,190 @@ +package balances + +import ( + "fmt" + "math" + "strconv" + "strings" + "testing" + + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/bip39" + "github.com/gnolang/gno/tm2/pkg/crypto/hd" + "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/jaekwon/testify/require" + "github.com/stretchr/testify/assert" +) + +func TestBalances_GetBalancesFromEntries(t *testing.T) { + t.Parallel() + + t.Run("valid balances", func(t *testing.T) { + t.Parallel() + + // Generate dummy keys + dummyKeys := getDummyKeys(t, 2) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) + + entries := make([]string, len(dummyKeys)) + + for index, key := range dummyKeys { + entries[index] = fmt.Sprintf( + "%s=%dugnot", + key.Address().String(), + amount.AmountOf("ugnot"), + ) + } + + balanceMap, err := GetBalancesFromEntries(entries...) + require.NoError(t, err) + + // Validate the balance map + assert.Len(t, balanceMap, len(dummyKeys)) + for _, key := range dummyKeys { + assert.Equal(t, amount, balanceMap[key.Address()].Amount) + } + }) + + t.Run("malformed balance, invalid format", func(t *testing.T) { + t.Parallel() + + entries := []string{ + "malformed balance", + } + + balanceMap, err := GetBalancesFromEntries(entries...) + + assert.Nil(t, balanceMap) + assert.ErrorContains(t, err, "malformed entry") + }) + + t.Run("malformed balance, invalid address", func(t *testing.T) { + t.Parallel() + + balances := []string{ + "dummyaddress=10ugnot", + } + + balanceMap, err := GetBalancesFromEntries(balances...) + + assert.Nil(t, balanceMap) + assert.ErrorContains(t, err, "invalid address") + }) + + t.Run("malformed balance, invalid amount", func(t *testing.T) { + t.Parallel() + + dummyKey := getDummyKey(t) + + balances := []string{ + fmt.Sprintf( + "%s=%sugnot", + dummyKey.Address().String(), + strconv.FormatUint(math.MaxUint64, 10), + ), + } + + balanceMap, err := GetBalancesFromEntries(balances...) + + assert.Nil(t, balanceMap) + assert.ErrorContains(t, err, "invalid amount") + }) +} + +func TestBalances_GetBalancesFromSheet(t *testing.T) { + t.Parallel() + + t.Run("valid balances", func(t *testing.T) { + t.Parallel() + + // Generate dummy keys + dummyKeys := getDummyKeys(t, 2) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) + + balances := make([]string, len(dummyKeys)) + + for index, key := range dummyKeys { + balances[index] = fmt.Sprintf( + "%s=%dugnot", + key.Address().String(), + amount.AmountOf("ugnot"), + ) + } + + reader := strings.NewReader(strings.Join(balances, "\n")) + balanceMap, err := GetBalancesFromSheet(reader) + require.NoError(t, err) + + // Validate the balance map + assert.Len(t, balanceMap, len(dummyKeys)) + for _, key := range dummyKeys { + assert.Equal(t, amount, balanceMap[key.Address()].Amount) + } + }) + + t.Run("malformed balance, invalid amount", func(t *testing.T) { + t.Parallel() + + dummyKey := getDummyKey(t) + + balances := []string{ + fmt.Sprintf( + "%s=%sugnot", + dummyKey.Address().String(), + strconv.FormatUint(math.MaxUint64, 10), + ), + } + + reader := strings.NewReader(strings.Join(balances, "\n")) + + balanceMap, err := GetBalancesFromSheet(reader) + + assert.Nil(t, balanceMap) + assert.ErrorContains(t, err, "invalid amount") + }) +} + +// 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 { + t.Helper() + + mnemonic, err := client.GenerateMnemonic(256) + require.NoError(t, err) + + seed := bip39.NewSeed(mnemonic, "") + + return generateKeyFromSeed(seed, 0).PubKey() +} + +// getDummyKeys generates random keys for testing +func getDummyKeys(t *testing.T, count int) []crypto.PubKey { + t.Helper() + + dummyKeys := make([]crypto.PubKey, count) + + for i := 0; i < count; i++ { + dummyKeys[i] = getDummyKey(t) + } + + return dummyKeys +} + +// generateKeyFromSeed generates a private key from +// the provided seed and index +func generateKeyFromSeed(seed []byte, index uint32) crypto.PrivKey { + pathParams := hd.NewFundraiserParams(0, crypto.CoinType, index) + + masterPriv, ch := hd.ComputeMastersFromSeed(seed) + + //nolint:errcheck // This derivation can never error out, since the path params + // are always going to be valid + derivedPriv, _ := hd.DerivePrivateKeyForPath(masterPriv, ch, pathParams.String()) + + return secp256k1.PrivKeySecp256k1(derivedPriv) +} From 9bb5de68c9fb4c1cd65bb3218fc89cc683260143 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:34:44 +0200 Subject: [PATCH 02/23] fix: cleanup Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/account.go | 41 ++++++ contribs/gnodev/main.go | 187 ++++++++++++++---------- contribs/gnodev/pkg/dev/keybase.go | 102 ------------- contribs/gnodev/pkg/dev/node.go | 14 +- contribs/gnodev/pkg/dev/packages.go | 18 +-- contribs/gnodev/pkg/rawterm/keypress.go | 1 + gno.land/pkg/gnoland/types.go | 90 ++++++++++++ gno.land/pkg/gnoland/types_test.go | 181 +++++++++++++++++++++++ 8 files changed, 440 insertions(+), 194 deletions(-) create mode 100644 contribs/gnodev/account.go delete mode 100644 contribs/gnodev/pkg/dev/keybase.go diff --git a/contribs/gnodev/account.go b/contribs/gnodev/account.go new file mode 100644 index 00000000000..d7e47e5f62b --- /dev/null +++ b/contribs/gnodev/account.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/gnolang/gno/tm2/pkg/std" +) + +type varAccounts map[string]std.Coins // name or bech32 -> coins + +func (va *varAccounts) Set(value string) error { + if *va == nil { + *va = map[string]std.Coins{} + } + accounts := *va + + user, amount, found := strings.Cut(value, ":") + accounts[user] = nil + if !found { + return nil + } + + coins, err := std.ParseCoins(amount) + if err != nil { + return fmt.Errorf("unable to parse coins from %q: %w", user, err) + } + + // Add the parsed amount into user + accounts[user] = coins + return nil +} + +func (va varAccounts) String() string { + accs := make([]string, 0, len(va)) + for user, balance := range va { + accs = append(accs, fmt.Sprintf("%s(%s)", user, balance.String())) + } + + return strings.Join(accs, ",") +} diff --git a/contribs/gnodev/main.go b/contribs/gnodev/main.go index 9bd52d89ec7..f024f4e0bae 100644 --- a/contribs/gnodev/main.go +++ b/contribs/gnodev/main.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "log/slog" - "math/rand" "net" "net/http" "os" @@ -29,10 +28,13 @@ import ( gnolog "github.com/gnolang/gno/gno.land/pkg/log" "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/bip39" "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/crypto/keys/keyerror" osm "github.com/gnolang/gno/tm2/pkg/os" "github.com/gnolang/gno/tm2/pkg/std" "github.com/muesli/termenv" @@ -60,21 +62,21 @@ type devCfg struct { nodeProxyAppListenerAddr string // Users default - genesisCreator string - home string - root string + genesisCreator string + home string + root string + additionalUsers varAccounts // Node Configuration - minimal bool - verbose bool - hotreload bool - noWatch bool - noReplay bool - maxGas int64 - chainId string - serverMode bool - balancesFile string - additionalUsers string + minimal bool + verbose bool + hotreload bool + noWatch bool + noReplay bool + maxGas int64 + chainId string + serverMode bool + balancesFile string } var defaultDevOptions = &devCfg{ @@ -142,18 +144,17 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { "listening address for GnoLand RPC node", ) - fs.StringVar( - &c.nodeRPCListenerAddr, - "add-users", - defaultDevOptions.nodeRPCListenerAddr, - "pre-add users, separated by commas", + fs.Var( + &c.additionalUsers, + "add-user", + "pre-add a user", ) fs.StringVar( &c.genesisCreator, - "creator", + "genesis-creator", defaultDevOptions.genesisCreator, - "name of the genesis creator (from Keybase) or address", + "name or bech32 address of the genesis creator", ) fs.BoolVar( @@ -228,7 +229,7 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { emitterServer := emitter.NewServer(loggerEvents) // load keybase - kb, err := setupKeybase(cfg) + kb, err := setupKeybase(cfg, logger) if err != nil { return fmt.Errorf("unable to load keybase: %w", err) } @@ -241,18 +242,14 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { // Setup Dev Node // XXX: find a good way to export or display node logs - devNode, err := setupDevNode(ctx, logger, cfg, emitterServer, kb, pkgpaths) + nodeLogger := logger.WithGroup(NodeLogName) + devNode, err := setupDevNode(ctx, nodeLogger, cfg, emitterServer, kb, pkgpaths) if err != nil { return err } defer devNode.Close() - logger.WithGroup(NodeLogName). - Info("node started", - "lisn", devNode.GetRemoteAddress(), - "addr", gnodev.DefaultCreator.String(), - "chainID", cfg.chainId, - ) + nodeLogger.Info("node started", "lisn", devNode.GetRemoteAddress(), "chainID", cfg.chainId) // Create server mux := http.NewServeMux() @@ -292,14 +289,17 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { // Add node pkgs to watcher watcher.AddPackages(devNode.ListPkgs()...) - logger.WithGroup("--- READY").Info("for commands and help, press `h`") + if !cfg.serverMode { + logger.WithGroup("--- READY").Info("for commands and help, press `h`") + } // Run the main event loop - return runEventLoop(ctx, logger, rt, devNode, watcher) + return runEventLoop(ctx, logger, kb, rt, devNode, watcher) } var helper string = ` -H Help - display this message +A Accounts - Display known accounts +H Help - Display this message R Reload - Reload all packages to take change into account. Ctrl+R Reset - Reset application state. Ctrl+C Exit - Exit the application @@ -308,6 +308,7 @@ Ctrl+C Exit - Exit the application func runEventLoop( ctx context.Context, logger *slog.Logger, + kb keys.Keybase, rt *rawterm.RawTerm, dnode *dev.Node, watch *watcher.PackageWatcher, @@ -346,9 +347,11 @@ func runEventLoop( ) switch key.Upper() { - case rawterm.KeyH: + case rawterm.KeyH: // Helper logger.Info("Gno Dev Helper", "helper", helper) - case rawterm.KeyR: + case rawterm.KeyA: // Accounts + logAccounts(logger.WithGroup("accounts"), kb, dnode) + case rawterm.KeyR: // Reload logger.WithGroup(NodeLogName).Info("reloading...") if err = dnode.ReloadAll(ctx); err != nil { logger.WithGroup(NodeLogName). @@ -356,17 +359,14 @@ func runEventLoop( } - case rawterm.KeyCtrlR: + case rawterm.KeyCtrlR: // Reset logger.WithGroup(NodeLogName).Info("reseting node state...") if err = dnode.Reset(ctx); err != nil { logger.WithGroup(NodeLogName). Error("unable to reset node state", "err", err) } - case rawterm.KeyCtrlC: - return nil - case rawterm.KeyCtrlE: - panic("NOOOO") + case rawterm.KeyCtrlC: // Exit return nil default: } @@ -449,12 +449,11 @@ func setupDevNode( kb keys.Keybase, pkgspath []dev.PackagePath, ) (*gnodev.Node, error) { - nodeLogger := logger.WithGroup(NodeLogName) - balances, err := generateBalances(kb, cfg) if err != nil { return nil, fmt.Errorf("unable to generate balances: %w", err) } + logger.Debug("balances loaded", "list", balances.List()) // configure gnoland node config := gnodev.DefaultNodeConfig(cfg.root) @@ -469,7 +468,7 @@ func setupDevNode( config.TMConfig.P2P.ListenAddress = defaultDevOptions.nodeP2PListenerAddr config.TMConfig.ProxyApp = defaultDevOptions.nodeProxyAppListenerAddr - return gnodev.NewDevNode(ctx, nodeLogger, remitter, config) + return gnodev.NewDevNode(ctx, logger, remitter, config) } // setupGnowebServer initializes and starts the Gnoweb server. @@ -501,6 +500,7 @@ func resolvePackagesPathFromArgs(cfg *devCfg, kb keys.Keybase, args []string) ([ return nil, fmt.Errorf("invalid package path/query %q: %w", arg, err) } + // Assign a default creator if user haven't specified it. if path.Creator.IsZero() { path.Creator = defaultKey.GetAddress() } @@ -508,9 +508,8 @@ func resolvePackagesPathFromArgs(cfg *devCfg, kb keys.Keybase, args []string) ([ paths[i] = path } - // Add examples folder if minimal specified + // Add examples folder if minimal is set to false if !cfg.minimal { - fmt.Println("adding example folder: ", filepath.Join(cfg.root, "examples")) paths = append(paths, gnodev.PackagePath{ Path: filepath.Join(cfg.root, "examples"), Creator: defaultKey.GetAddress(), @@ -523,17 +522,23 @@ func resolvePackagesPathFromArgs(cfg *devCfg, kb keys.Keybase, args []string) ([ func generateBalances(kb keys.Keybase, cfg *devCfg) (balances.Balances, error) { bls := balances.New() - amount := std.Coins{std.NewCoin("ugnot", 10e6)} + unlimitedFund := std.Coins{std.NewCoin("ugnot", 10e12)} keys, err := kb.List() if err != nil { return nil, fmt.Errorf("unable to list keys from keybase: %w", err) } - // Automatically set every key from keybase to unlimited found + // Automatically set every key from keybase to unlimited found (or pre + // defined found if specified) for _, key := range keys { + found := unlimitedFund + if preDefinedFound, ok := cfg.additionalUsers[key.GetName()]; ok && preDefinedFound != nil { + found = preDefinedFound + } + address := key.GetAddress() - bls[address] = gnoland.Balance{Amount: amount, Address: address} + bls[address] = gnoland.Balance{Amount: found, Address: address} } if cfg.balancesFile == "" { @@ -550,12 +555,12 @@ func generateBalances(kb keys.Keybase, cfg *devCfg) (balances.Balances, error) { return nil, fmt.Errorf("unable to read balances file %q: %w", cfg.balancesFile, err) } - // Left merge keybase balance into file balance + // Left merge keybase balance into loaded file balance blsFile.LeftMerge(bls) return blsFile, nil } -func listenForKeyPress(w io.Writer, rt *rawterm.RawTerm) <-chan rawterm.KeyPress { +func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm.KeyPress { cc := make(chan rawterm.KeyPress, 1) go func() { defer close(cc) @@ -586,15 +591,6 @@ func createAccount(kb keys.Keybase, accountName string) (keys.Info, error) { return kb.CreateAccount(accountName, mnemonic, "", "", 0, 0) } -func checkForError(w io.Writer, err error) { - if err != nil { - fmt.Fprintf(w, "[ERROR] - %s\n", err.Error()) - return - } - - fmt.Fprintln(w, "[DONE]") -} - func resolveUnixOrTCPAddr(in string) (out string) { var err error var addr net.Addr @@ -618,7 +614,7 @@ func resolveUnixOrTCPAddr(in string) (out string) { panic(err) } -func setupKeybase(cfg *devCfg) (keys.Keybase, error) { +func setupKeybase(cfg *devCfg, logger *slog.Logger) (keys.Keybase, error) { kb := keys.NewInMemory() if cfg.home != "" { // load home keybase into our inMemory keybase @@ -646,34 +642,75 @@ func setupKeybase(cfg *devCfg) (keys.Keybase, error) { } // Add additional users to our keybase - addUsers := strings.Split(cfg.additionalUsers, ",") - for _, user := range addUsers { - if _, err := createAccount(kb, user); err != nil { + for user := range cfg.additionalUsers { + info, err := createAccount(kb, user) + if err != nil { return nil, fmt.Errorf("unable to create user %q: %w", user, err) } - } - // Add default creator it doesn't exist - ok, _ := kb.HasByAddress(DefaultCreatorAddress) - if ok { - return kb, nil + logger.Info("additional user", "name", info.GetName(), "addr", info.GetAddress()) } - for i := 0; i < 5; i++ { - creatorName := fmt.Sprintf("_testUser%05d\n", rand.Intn(100000)) - ok, _ := kb.HasByName(creatorName) - if ok { - continue + // Next, make sure that we have a default address to load packages + var info keys.Info + var err error + + info, err = kb.GetByNameOrAddress(cfg.genesisCreator) + switch { + case err == nil: // user already have a default user + case keyerror.IsErrKeyNotFound(err): + // if the key isn't found, create a default one + creatorName := fmt.Sprintf("_default#%.10s", DefaultCreatorAddress.String()) + if ok, _ := kb.HasByName(creatorName); ok { + // if a collision happen here, someone really want to not run. + return nil, fmt.Errorf("unable to create creator account, delete %q from your keybase", creatorName) } - if _, err := kb.CreateAccount(creatorName, DefaultCreatorSeed, "", "", 0, 0); err != nil { + info, err = kb.CreateAccount(creatorName, DefaultCreatorSeed, "", "", 0, 0) + if err != nil { return nil, fmt.Errorf("unable to create default %q account: %w", DefaultCreatorName, err) } + default: + return nil, fmt.Errorf("unable to get address %q from keybase: %w", info.GetAddress(), err) + } + + logger.Info("default creator", "name", info.GetName(), "addr", info.GetAddress()) + return kb, nil +} + +func logAccounts(logger *slog.Logger, kb keys.Keybase, _ *dev.Node) error { + keys, err := kb.List() + if err != nil { + return fmt.Errorf("unable to get keybase keys list: %w", err) + } + + accounts := make([]string, len(keys)) + for i, key := range keys { + if key.GetName() == "" { + continue // skip empty key name + } + + address := key.GetAddress() + qres, err := client.NewLocal().ABCIQuery("auth/accounts/"+address.String(), []byte{}) + if err != nil { + return fmt.Errorf("unable to querry account %q: %w", address.String(), err) + } + + var qret struct{ BaseAccount std.BaseAccount } + if err = amino.UnmarshalJSON(qres.Response.Data, &qret); err != nil { + return fmt.Errorf("unable to unmarshal query response: %w", err) + } - return kb, nil + // format name - (address) -> (coins) -> (acct-num) -> (seq) + accounts[i] = fmt.Sprintf("%s: addr(%s) coins(%s) acct_num(%d)", + key.GetName(), + address.String(), + qret.BaseAccount.GetCoins().String(), + qret.BaseAccount.GetAccountNumber()) } - panic("unable to generate ranmdom test user name") + logger.Info("current accounts", "balances", strings.Join(accounts, "\n")) + return nil } func setuplogger(cfg *devCfg, out io.Writer) *slog.Logger { diff --git a/contribs/gnodev/pkg/dev/keybase.go b/contribs/gnodev/pkg/dev/keybase.go deleted file mode 100644 index 7f69a36c31b..00000000000 --- a/contribs/gnodev/pkg/dev/keybase.go +++ /dev/null @@ -1,102 +0,0 @@ -package dev - -import ( - "fmt" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/crypto/bip39" - "github.com/gnolang/gno/tm2/pkg/crypto/keys" - "github.com/gnolang/gno/tm2/pkg/std" -) - -// type Keybase struct { -// keys.Keybase -// } - -// func NewKeybase() *Keybase { -// return &Keybase{ -// Keybase: keys.NewInMemory(), -// } -// } - -// func (to *Keybase) ImportKeybaseFromPath(path string) error { -// from, err := keys.NewKeyBaseFromDir(path) -// if err != nil { -// return fmt.Errorf("unable to load keybase: %w", err) -// } - -// keys, err := from.List() -// if err != nil { -// return fmt.Errorf("unable to list keys: %w", path, err) -// } - -// for _, key := range keys { -// armor, err := from.Export(key.GetName()) -// if err != nil { -// return fmt.Errorf("unable to import key %q: %w", key.GetName(), err) -// } - -// err = to.Import(key.GetName(), armor) -// if err != nil { -// return fmt.Errorf("unable to import key %q: %w", key.GetName(), err) -// } -// } - -// return nil -// } - -// type PackagePath struct { -// Path string -// CreatorNameOrAddress string -// } - -// func ParsePackagePath(path string) (PackagePath, error) { -// var ppath PackagePath - -// upath, err := url.Parse(path) -// if err != nil { -// return ppath, fmt.Errorf("unable to parse package path: %w", err) -// } - -// // Get path -// ppath.Path = filepath.Clean(upath.Path) -// // Check for options -// ppath.CreatorNameOrAddress = upath.Query().Get("creator") -// return ppath, nil -// } - -// func LoadKeyabaseBalanceFromPath(kb keys.Keybase) ([]gnoland.Balance, error) { -// keys, err := kb.List() -// if err != nil { -// return nil, nil -// } - -// for _, info := range keys { -// info.GetName() -// } -// } - -// loadAccount with the given name and adds it to the keybase. -func loadAccount(kb keys.Keybase, accountName string) (gnoland.Balance, error) { - var balance gnoland.Balance - entropy, err := bip39.NewEntropy(256) - if err != nil { - return balance, fmt.Errorf("error creating entropy: %w", err) - } - - mnemonic, err := bip39.NewMnemonic(entropy) - if err != nil { - return balance, fmt.Errorf("error generating mnemonic: %w", err) - } - - var keyInfo keys.Info - if keyInfo, err = kb.CreateAccount(accountName, mnemonic, "", "", 0, 0); err != nil { - return balance, fmt.Errorf("unable to create account: %w", err) - } - - address := keyInfo.GetAddress() - return gnoland.Balance{ - Address: address, - Amount: std.Coins{std.NewCoin("ugnot", 10e6)}, - }, nil -} diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index baa8c3626df..786c9349d0e 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -46,7 +46,7 @@ func DefaultNodeConfig(rootdir string) *NodeConfig { balances := []gnoland.Balance{ { Address: defaultCreator, - Amount: std.Coins{std.NewCoin("ugnot", 10e6)}, + Amount: std.Coins{std.NewCoin("ugnot", 10e12)}, }, } @@ -83,7 +83,7 @@ func NewDevNode(ctx context.Context, logger *slog.Logger, emitter emitter.Emitte return nil, fmt.Errorf("unable map pkgs list: %w", err) } - pkgsTxs, err := mpkgs.Load(cfg.DefaultCreator, DefaultFee) + pkgsTxs, err := mpkgs.Load(DefaultFee) if err != nil { return nil, fmt.Errorf("unable to load genesis packages: %w", err) } @@ -186,7 +186,7 @@ func (d *Node) Reset(ctx context.Context) error { } // Generate a new genesis state based on the current packages - txs, err := d.pkgs.Load(d.config.DefaultCreator, DefaultFee) + txs, err := d.pkgs.Load(DefaultFee) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } @@ -244,7 +244,7 @@ func (d *Node) Reload(ctx context.Context) error { } // Load genesis packages - pkgsTxs, err := d.pkgs.Load(d.config.DefaultCreator, DefaultFee) + pkgsTxs, err := d.pkgs.Load(DefaultFee) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } @@ -318,6 +318,12 @@ func (d *Node) GetBlockTransactions(blockNum uint64) ([]std.Tx, error) { return txs, nil } +// GetBlockTransactions returns the transactions contained +// within the specified block, if any +func (d *Node) CurrentBalances(blockNum uint64) ([]std.Tx, error) { + return nil, nil +} + // GetBlockTransactions returns the transactions contained // within the specified block, if any // GetLatestBlockNumber returns the latest block height from the chain diff --git a/contribs/gnodev/pkg/dev/packages.go b/contribs/gnodev/pkg/dev/packages.go index 65acb8ec585..bf69338b2b5 100644 --- a/contribs/gnodev/pkg/dev/packages.go +++ b/contribs/gnodev/pkg/dev/packages.go @@ -9,7 +9,6 @@ import ( 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" - bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/std" @@ -24,7 +23,7 @@ type PackagePath struct { func ResolvePackagePathQuery(kb keys.Keybase, path string) (PackagePath, error) { var ppath PackagePath - upath, err := url.Parse(filepath.Clean(path)) + upath, err := url.Parse(path) if err != nil { return ppath, fmt.Errorf("malformed path/query: %w", err) } @@ -78,25 +77,15 @@ var ( func NewPackagesMap(ppaths []PackagePath) (PackagesMap, error) { pkgs := make(map[string]Package) for _, ppath := range ppaths { - fmt.Println(ppath) if ppath.Creator.IsZero() { return nil, fmt.Errorf("unable to load package %q: %w", ppath.Path, ErrEmptyCreatorPackage) } - // if ppath.Deposit.Empty() { - // return nil, fmt.Errorf("unable to load package %q: %w", ppath.Path, ErrEmptyDepositPackage) - // } - abspath, err := filepath.Abs(ppath.Path) if err != nil { return nil, fmt.Errorf("unable to guess absolute path for %q: %w", ppath.Path, err) } - // rootdir, err := gnomod.FindRootDir(abspath) - // if err != nil { - // return nil, fmt.Errorf("unable to find rootdir for package %q: %w", ppath.Path, err) - // } - // list all packages from target path pkgslist, err := gnomod.ListPkgs(abspath) if err != nil { @@ -130,7 +119,7 @@ func (pm PackagesMap) toList() gnomod.PkgList { return list } -func (pm PackagesMap) Load(creator bft.Address, fee std.Fee) ([]std.Tx, error) { +func (pm PackagesMap) Load(fee std.Fee) ([]std.Tx, error) { pkgs := pm.toList() sorted, err := pkgs.Sort() @@ -142,6 +131,9 @@ func (pm PackagesMap) Load(creator bft.Address, fee std.Fee) ([]std.Tx, error) { txs := []std.Tx{} for _, modPkg := range nonDraft { pkg := pm[modPkg.Dir] + if pkg.Creator.IsZero() { + return nil, fmt.Errorf("no creator was set for %q", pkg.Dir) + } // Open files in directory as MemPackage. memPkg := gno.ReadMemPackage(modPkg.Dir, modPkg.Name) diff --git a/contribs/gnodev/pkg/rawterm/keypress.go b/contribs/gnodev/pkg/rawterm/keypress.go index 20503476a9b..48f8367a65b 100644 --- a/contribs/gnodev/pkg/rawterm/keypress.go +++ b/contribs/gnodev/pkg/rawterm/keypress.go @@ -18,6 +18,7 @@ const ( KeyCtrlR KeyPress = '\x12' // Ctrl+R KeyCtrlT KeyPress = '\x14' // Ctrl+T + KeyA KeyPress = 'A' KeyH KeyPress = 'H' KeyR KeyPress = 'R' ) diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index 5d68064c9c5..da6bef97746 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -1,8 +1,10 @@ package gnoland import ( + "bufio" "errors" "fmt" + "io" "strings" bft "github.com/gnolang/gno/tm2/pkg/bft/types" @@ -77,3 +79,91 @@ func (b Balance) MarshalAmino() (string, error) { func (b Balance) String() string { return fmt.Sprintf("%s=%s", b.Address.String(), b.Amount.String()) } + +type Balances map[crypto.Address]Balance + +func New() Balances { + return make(Balances) +} + +func (balances Balances) Set(address crypto.Address, amount std.Coins) { + balances[address] = Balance{ + Address: address, + Amount: amount, + } +} + +func (balances Balances) Get(address crypto.Address) (balance Balance, ok bool) { + balance, ok = balances[address] + return +} + +func (balances Balances) List() []Balance { + list := make([]Balance, 0, len(balances)) + for _, balance := range balances { + list = append(list, balance) + } + return list +} + +// leftMerge left-merges the two maps +func (a Balances) LeftMerge(b Balances) { + for key, bVal := range b { + if _, present := (a)[key]; !present { + (a)[key] = bVal + } + } +} + +func GetBalancesFromEntries(entries ...string) (Balances, error) { + balances := New() + return balances, balances.LoadFromEntries(entries...) +} + +// LoadFromEntries extracts the balance entries in the form of
= +func (balances Balances) LoadFromEntries(entries ...string) error { + for _, entry := range entries { + var balance Balance + if err := balance.Parse(entry); err != nil { + return fmt.Errorf("unable to parse balance entry: %w", err) + } + balances[balance.Address] = balance + } + + return nil +} + +func GetBalancesFromSheet(sheet io.Reader) (Balances, error) { + balances := New() + return balances, balances.LoadFromSheet(sheet) +} + +// LoadFromSheet extracts the balance sheet from the passed in +// balance sheet file, that has the format of
=ugnot +func (balances Balances) LoadFromSheet(sheet io.Reader) error { + // Parse the balances + scanner := bufio.NewScanner(sheet) + + for scanner.Scan() { + entry := scanner.Text() + + // Remove comments + entry = strings.Split(entry, "#")[0] + entry = strings.TrimSpace(entry) + + // Skip empty lines + if entry == "" { + continue + } + + if err := balances.LoadFromEntries(entry); err != nil { + return fmt.Errorf("unable to load entries: %w", err) + } + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("error encountered while scanning, %w", err) + } + + return nil +} diff --git a/gno.land/pkg/gnoland/types_test.go b/gno.land/pkg/gnoland/types_test.go index 97222d0cdfd..00e5d46d996 100644 --- a/gno.land/pkg/gnoland/types_test.go +++ b/gno.land/pkg/gnoland/types_test.go @@ -2,11 +2,18 @@ package gnoland import ( "fmt" + "math" + "strconv" + "strings" "testing" "github.com/gnolang/gno/tm2/pkg/amino" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/bip39" + "github.com/gnolang/gno/tm2/pkg/crypto/hd" + "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" "github.com/gnolang/gno/tm2/pkg/std" "github.com/jaekwon/testify/assert" "github.com/jaekwon/testify/require" @@ -96,3 +103,177 @@ func TestBalance_AminoMarshalJSON(t *testing.T) { require.NoError(t, err) require.JSONEq(t, expectedJSON, string(balancesJSON)) } + +func TestBalances_GetBalancesFromEntries(t *testing.T) { + t.Parallel() + + t.Run("valid balances", func(t *testing.T) { + t.Parallel() + + // Generate dummy keys + dummyKeys := getDummyKeys(t, 2) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) + + entries := make([]string, len(dummyKeys)) + + for index, key := range dummyKeys { + entries[index] = fmt.Sprintf( + "%s=%dugnot", + key.Address().String(), + amount.AmountOf("ugnot"), + ) + } + + balanceMap, err := GetBalancesFromEntries(entries...) + require.NoError(t, err) + + // Validate the balance map + assert.Len(t, balanceMap, len(dummyKeys)) + for _, key := range dummyKeys { + assert.Equal(t, amount, balanceMap[key.Address()].Amount) + } + }) + + t.Run("malformed balance, invalid format", func(t *testing.T) { + t.Parallel() + + entries := []string{ + "malformed balance", + } + + balanceMap, err := GetBalancesFromEntries(entries...) + require.NoError(t, err) + + assert.Nil(t, balanceMap) + assert.Contains(t, err.Error(), "malformed entry") + }) + + t.Run("malformed balance, invalid address", func(t *testing.T) { + t.Parallel() + + balances := []string{ + "dummyaddress=10ugnot", + } + + balanceMap, err := GetBalancesFromEntries(balances...) + require.NoError(t, err) + + assert.Nil(t, balanceMap) + assert.Contains(t, err.Error(), "invalid address") + }) + + t.Run("malformed balance, invalid amount", func(t *testing.T) { + t.Parallel() + + dummyKey := getDummyKey(t) + + balances := []string{ + fmt.Sprintf( + "%s=%sugnot", + dummyKey.Address().String(), + strconv.FormatUint(math.MaxUint64, 10), + ), + } + + balanceMap, err := GetBalancesFromEntries(balances...) + + assert.Nil(t, balanceMap) + assert.ErrorContains(t, err, "invalid amount") + }) +} + +func TestBalances_GetBalancesFromSheet(t *testing.T) { + t.Parallel() + + t.Run("valid balances", func(t *testing.T) { + t.Parallel() + + // Generate dummy keys + dummyKeys := getDummyKeys(t, 2) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) + + balances := make([]string, len(dummyKeys)) + + for index, key := range dummyKeys { + balances[index] = fmt.Sprintf( + "%s=%dugnot", + key.Address().String(), + amount.AmountOf("ugnot"), + ) + } + + reader := strings.NewReader(strings.Join(balances, "\n")) + balanceMap, err := GetBalancesFromSheet(reader) + require.NoError(t, err) + + // Validate the balance map + assert.Len(t, balanceMap, len(dummyKeys)) + for _, key := range dummyKeys { + assert.Equal(t, amount, balanceMap[key.Address()].Amount) + } + }) + + t.Run("malformed balance, invalid amount", func(t *testing.T) { + t.Parallel() + + dummyKey := getDummyKey(t) + + balances := []string{ + fmt.Sprintf( + "%s=%sugnot", + dummyKey.Address().String(), + strconv.FormatUint(math.MaxUint64, 10), + ), + } + + reader := strings.NewReader(strings.Join(balances, "\n")) + + balanceMap, err := GetBalancesFromSheet(reader) + + assert.Nil(t, balanceMap) + assert.ErrorContains(t, err, "invalid amount") + }) +} + +// 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 { + t.Helper() + + mnemonic, err := client.GenerateMnemonic(256) + require.NoError(t, err) + + seed := bip39.NewSeed(mnemonic, "") + + return generateKeyFromSeed(seed, 0).PubKey() +} + +// getDummyKeys generates random keys for testing +func getDummyKeys(t *testing.T, count int) []crypto.PubKey { + t.Helper() + + dummyKeys := make([]crypto.PubKey, count) + + for i := 0; i < count; i++ { + dummyKeys[i] = getDummyKey(t) + } + + return dummyKeys +} + +// generateKeyFromSeed generates a private key from +// the provided seed and index +func generateKeyFromSeed(seed []byte, index uint32) crypto.PrivKey { + pathParams := hd.NewFundraiserParams(0, crypto.CoinType, index) + + masterPriv, ch := hd.ComputeMastersFromSeed(seed) + + //nolint:errcheck // This derivation can never error out, since the path params + // are always going to be valid + derivedPriv, _ := hd.DerivePrivateKeyForPath(masterPriv, ch, pathParams.String()) + + return secp256k1.PrivKeySecp256k1(derivedPriv) +} From ca194ef8a299a5646faca6f6fb65ad1ece1a3255 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:18:06 +0200 Subject: [PATCH 03/23] fix: cleanup and move gnodev into /cmd Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/Makefile | 4 +- contribs/gnodev/account.go | 41 -- contribs/gnodev/cmd/gnodev/accounts.go | 208 +++++++ contribs/gnodev/cmd/gnodev/logger.go | 35 ++ contribs/gnodev/cmd/gnodev/main.go | 413 +++++++++++++ contribs/gnodev/cmd/gnodev/setup_node.go | 73 +++ contribs/gnodev/cmd/gnodev/setup_term.go | 24 + contribs/gnodev/cmd/gnodev/setup_web.go | 20 + contribs/gnodev/main.go | 738 ----------------------- docs/gno-tooling/cli/gnodev.md | 50 +- gno.land/pkg/balances/balances.go | 100 --- gno.land/pkg/balances/balances_test.go | 190 ------ gno.land/pkg/gnoland/types.go | 6 +- 13 files changed, 819 insertions(+), 1083 deletions(-) delete mode 100644 contribs/gnodev/account.go create mode 100644 contribs/gnodev/cmd/gnodev/accounts.go create mode 100644 contribs/gnodev/cmd/gnodev/logger.go create mode 100644 contribs/gnodev/cmd/gnodev/main.go create mode 100644 contribs/gnodev/cmd/gnodev/setup_node.go create mode 100644 contribs/gnodev/cmd/gnodev/setup_term.go create mode 100644 contribs/gnodev/cmd/gnodev/setup_web.go delete mode 100644 contribs/gnodev/main.go delete mode 100644 gno.land/pkg/balances/balances.go delete mode 100644 gno.land/pkg/balances/balances_test.go diff --git a/contribs/gnodev/Makefile b/contribs/gnodev/Makefile index 23fb22a372d..7395b166a3e 100644 --- a/contribs/gnodev/Makefile +++ b/contribs/gnodev/Makefile @@ -3,7 +3,7 @@ GNOROOT_DIR ?= $(abspath $(lastword $(MAKEFILE_LIST))/../../../) GOBUILD_FLAGS := -ldflags "-X github.com/gnolang/gno/gnovm/pkg/gnoenv._GNOROOT=$(GNOROOT_DIR)" install: - go install $(GOBUILD_FLAGS) . + go install $(GOBUILD_FLAGS) ./cmd/gnodev build: - go build $(GOBUILD_FLAGS) -o build/gnodev ./cmd/gno + go build $(GOBUILD_FLAGS) -o build/gnodev ./cmd/gnodev diff --git a/contribs/gnodev/account.go b/contribs/gnodev/account.go deleted file mode 100644 index d7e47e5f62b..00000000000 --- a/contribs/gnodev/account.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "fmt" - "strings" - - "github.com/gnolang/gno/tm2/pkg/std" -) - -type varAccounts map[string]std.Coins // name or bech32 -> coins - -func (va *varAccounts) Set(value string) error { - if *va == nil { - *va = map[string]std.Coins{} - } - accounts := *va - - user, amount, found := strings.Cut(value, ":") - accounts[user] = nil - if !found { - return nil - } - - coins, err := std.ParseCoins(amount) - if err != nil { - return fmt.Errorf("unable to parse coins from %q: %w", user, err) - } - - // Add the parsed amount into user - accounts[user] = coins - return nil -} - -func (va varAccounts) String() string { - accs := make([]string, 0, len(va)) - for user, balance := range va { - accs = append(accs, fmt.Sprintf("%s(%s)", user, balance.String())) - } - - return strings.Join(accs, ",") -} diff --git a/contribs/gnodev/cmd/gnodev/accounts.go b/contribs/gnodev/cmd/gnodev/accounts.go new file mode 100644 index 00000000000..3335cd7fde9 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/accounts.go @@ -0,0 +1,208 @@ +package main + +import ( + "fmt" + "log/slog" + "os" + "strings" + "text/tabwriter" + + "github.com/gnolang/gno/contribs/gnodev/pkg/dev" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + "github.com/gnolang/gno/tm2/pkg/crypto/bip39" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/crypto/keys/keyerror" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type varAccounts map[string]std.Coins // name or bech32 -> coins + +func (va *varAccounts) Set(value string) error { + if *va == nil { + *va = map[string]std.Coins{} + } + accounts := *va + + user, amount, found := strings.Cut(value, ":") + accounts[user] = nil + if !found { + return nil + } + + coins, err := std.ParseCoins(amount) + if err != nil { + return fmt.Errorf("unable to parse coins from %q: %w", user, err) + } + + // Add the parsed amount into user + accounts[user] = coins + return nil +} + +func (va varAccounts) String() string { + accs := make([]string, 0, len(va)) + for user, balance := range va { + accs = append(accs, fmt.Sprintf("%s(%s)", user, balance.String())) + } + + return strings.Join(accs, ",") +} + +func setupKeybase(logger *slog.Logger, cfg *devCfg) (keys.Keybase, error) { + kb := keys.NewInMemory() + if cfg.home != "" { + // Load home keybase into our inMemory keybase + kbHome, err := keys.NewKeyBaseFromDir(cfg.home) + if err != nil { + return nil, fmt.Errorf("unable to load keybase from dir %q: %w", cfg.home, err) + } + + keys, err := kbHome.List() + if err != nil { + return nil, fmt.Errorf("unable to list keys from keybase %q: %w", cfg.home, err) + } + + for _, key := range keys { + name := key.GetName() + armor, err := kbHome.Export(name) + if err != nil { + return nil, fmt.Errorf("unable to export key %q: %w", name, err) + } + + if err := kb.Import(name, armor); err != nil { + return nil, fmt.Errorf("unable to import key %q: %w", name, err) + } + } + } + + // Add additional users to our keybase + for user := range cfg.additionalUsers { + info, err := createAccount(kb, user) + if err != nil { + return nil, fmt.Errorf("unable to create user %q: %w", user, err) + } + + logger.Info("additional user", "name", info.GetName(), "addr", info.GetAddress()) + } + + // Next, make sure that we have a default address to load packages + info, err := kb.GetByNameOrAddress(cfg.genesisCreator) + switch { + case err == nil: // user already have a default user + break + case keyerror.IsErrKeyNotFound(err): + // If the key isn't found, create a default one + creatorName := fmt.Sprintf("_default#%.10s", DefaultCreatorAddress.String()) + if ok, _ := kb.HasByName(creatorName); ok { + return nil, fmt.Errorf("unable to create creator account, delete %q from your keybase", creatorName) + } + + info, err = kb.CreateAccount(creatorName, DefaultCreatorSeed, "", "", 0, 0) + if err != nil { + return nil, fmt.Errorf("unable to create default %q account: %w", DefaultCreatorName, err) + } + default: + return nil, fmt.Errorf("unable to get address %q from keybase: %w", info.GetAddress(), err) + } + + logger.Info("default creator", "name", info.GetName(), "addr", info.GetAddress()) + return kb, nil +} + +func generateBalances(kb keys.Keybase, cfg *devCfg) (gnoland.Balances, error) { + bls := gnoland.NewBalances() + unlimitedFund := std.Coins{std.NewCoin("ugnot", 10e12)} + + keys, err := kb.List() + if err != nil { + return nil, fmt.Errorf("unable to list keys from keybase: %w", err) + } + + // Automatically set every key from keybase to unlimited found (or pre + // defined found if specified) + for _, key := range keys { + found := unlimitedFund + if preDefinedFound, ok := cfg.additionalUsers[key.GetName()]; ok && preDefinedFound != nil { + found = preDefinedFound + } + + address := key.GetAddress() + bls[address] = gnoland.Balance{Amount: found, Address: address} + } + + if cfg.balancesFile == "" { + return bls, nil + } + + file, err := os.Open(cfg.balancesFile) + if err != nil { + return nil, fmt.Errorf("unable to open balance file %q: %w", cfg.balancesFile, err) + } + + blsFile, err := gnoland.GetBalancesFromSheet(file) + if err != nil { + return nil, fmt.Errorf("unable to read balances file %q: %w", cfg.balancesFile, err) + } + + // Left merge keybase balance into loaded file balance + blsFile.LeftMerge(bls) + return blsFile, nil +} + +func logAccounts(logger *slog.Logger, kb keys.Keybase, _ *dev.Node) error { + keys, err := kb.List() + if err != nil { + return fmt.Errorf("unable to get keybase keys list: %w", err) + } + + var tab strings.Builder + tab.WriteRune('\n') + tabw := tabwriter.NewWriter(&tab, 0, 0, 2, ' ', tabwriter.TabIndent) + + fmt.Fprintln(tabw, "KeyName\tAddress\tBalance") // Table header + for _, key := range keys { + if key.GetName() == "" { + continue // skip empty key name + } + + address := key.GetAddress() + // XXX: use client from node from argument, should be exposed by the node directly + qres, err := client.NewLocal().ABCIQuery("auth/accounts/"+address.String(), []byte{}) + if err != nil { + return fmt.Errorf("unable to querry account %q: %w", address.String(), err) + } + + var qret struct{ BaseAccount std.BaseAccount } + if err = amino.UnmarshalJSON(qres.Response.Data, &qret); err != nil { + return fmt.Errorf("unable to unmarshal query response: %w", err) + } + + // Insert row with name, addr, balance amount + fmt.Fprintf(tabw, "%s\t%s\t%s\n", key.GetName(), + address.String(), + qret.BaseAccount.GetCoins().String()) + } + // Flush table + tabw.Flush() + + headline := fmt.Sprintf("(%d) known accounts", len(keys)) + logger.Info(headline, "table", tab.String()) + return nil +} + +// createAccount creates a new account with the given name and adds it to the keybase. +func createAccount(kb keys.Keybase, accountName string) (keys.Info, error) { + entropy, err := bip39.NewEntropy(256) + if err != nil { + return nil, fmt.Errorf("error creating entropy: %w", err) + } + + mnemonic, err := bip39.NewMnemonic(entropy) + if err != nil { + return nil, fmt.Errorf("error generating mnemonic: %w", err) + } + + return kb.CreateAccount(accountName, mnemonic, "", "", 0, 0) +} diff --git a/contribs/gnodev/cmd/gnodev/logger.go b/contribs/gnodev/cmd/gnodev/logger.go new file mode 100644 index 00000000000..9e69654f478 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/logger.go @@ -0,0 +1,35 @@ +package main + +import ( + "io" + "log/slog" + + "github.com/charmbracelet/lipgloss" + "github.com/gnolang/gno/contribs/gnodev/pkg/logger" + gnolog "github.com/gnolang/gno/gno.land/pkg/log" + "github.com/muesli/termenv" +) + +func setuplogger(cfg *devCfg, out io.Writer) *slog.Logger { + level := slog.LevelInfo + if cfg.verbose { + level = slog.LevelDebug + } + + if cfg.serverMode { + zaplogger := logger.NewZapLogger(out, level) + return gnolog.ZapLoggerToSlog(zaplogger) + } + + // Detect term color profile + colorProfile := termenv.DefaultOutput().Profile + clogger := logger.NewColumnLogger(out, level, colorProfile) + + // Register well known group color with system colors + clogger.RegisterGroupColor(NodeLogName, lipgloss.Color("3")) + clogger.RegisterGroupColor(WebLogName, lipgloss.Color("4")) + clogger.RegisterGroupColor(KeyPressLogName, lipgloss.Color("5")) + clogger.RegisterGroupColor(EventServerLogName, lipgloss.Color("6")) + + return slog.New(clogger) +} diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go new file mode 100644 index 00000000000..648fe78b20f --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -0,0 +1,413 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log/slog" + "net/http" + "os" + "path/filepath" + + "github.com/gnolang/gno/contribs/gnodev/pkg/dev" + gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" + "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" + "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" + "github.com/gnolang/gno/contribs/gnodev/pkg/watcher" + "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + osm "github.com/gnolang/gno/tm2/pkg/os" +) + +const ( + NodeLogName = "Node" + WebLogName = "GnoWeb" + KeyPressLogName = "KeyPress" + EventServerLogName = "Event" +) + +var ( + DefaultCreatorName = integration.DefaultAccount_Name + DefaultCreatorAddress = crypto.MustAddressFromString(integration.DefaultAccount_Address) + DefaultCreatorSeed = integration.DefaultAccount_Seed +) + +type devCfg struct { + // Listeners + webListenerAddr string + nodeRPCListenerAddr string + nodeP2PListenerAddr string + nodeProxyAppListenerAddr string + + // Users default + genesisCreator string + home string + root string + additionalUsers varAccounts + balancesFile string + + // Node Configuration + minimal bool + verbose bool + hotreload bool + noWatch bool + noReplay bool + maxGas int64 + chainId string + serverMode bool +} + +var defaultDevOptions = &devCfg{ + chainId: "dev", + maxGas: 10_000_000_000, + webListenerAddr: "127.0.0.1:8888", + nodeRPCListenerAddr: "127.0.0.1:36657", + genesisCreator: DefaultCreatorAddress.String(), + home: gnoenv.HomeDir(), + root: gnoenv.RootDir(), + + // As we have no reason to configure this yet, set this to random port + // to avoid potential conflict with other app + nodeP2PListenerAddr: "tcp://127.0.0.1:0", + nodeProxyAppListenerAddr: "tcp://127.0.0.1:0", +} + +func main() { + cfg := &devCfg{} + + stdio := commands.NewDefaultIO() + cmd := commands.NewCommand( + commands.Metadata{ + Name: "gnodev", + ShortUsage: "gnodev [flags] [path ...]", + ShortHelp: "runs an in-memory node and gno.land web server for development purposes.", + LongHelp: `The gnodev command starts an in-memory node and a gno.land web interface +primarily for realm package development. It automatically loads the 'examples' directory and any +additional specified paths.`, + }, + cfg, + func(_ context.Context, args []string) error { + return execDev(cfg, args, stdio) + }) + + cmd.Execute(context.Background(), os.Args[1:]) +} + +func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.home, + "home", + defaultDevOptions.home, + "user's local directory for keys", + ) + + fs.StringVar( + &c.root, + "root", + defaultDevOptions.root, + "gno root directory", + ) + + fs.StringVar( + &c.webListenerAddr, + "web-listener", + defaultDevOptions.webListenerAddr, + "web server listening address", + ) + + fs.StringVar( + &c.nodeRPCListenerAddr, + "node-rpc-listener", + defaultDevOptions.nodeRPCListenerAddr, + "listening address for GnoLand RPC node", + ) + + fs.Var( + &c.additionalUsers, + "add-user", + "pre-add a user", + ) + + fs.StringVar( + &c.genesisCreator, + "genesis-creator", + defaultDevOptions.genesisCreator, + "name or bech32 address of the genesis creator", + ) + + fs.BoolVar( + &c.minimal, + "minimal", + defaultDevOptions.minimal, + "do not load packages from the examples directory", + ) + + fs.BoolVar( + &c.serverMode, + "server-mode", + defaultDevOptions.serverMode, + "disable interaction, and adjust logging for server use.", + ) + + fs.BoolVar( + &c.verbose, + "verbose", + defaultDevOptions.verbose, + "enable verbose output for development", + ) + + fs.StringVar( + &c.chainId, + "chain-id", + defaultDevOptions.chainId, + "set node ChainID", + ) + + fs.BoolVar( + &c.noWatch, + "no-watch", + defaultDevOptions.noWatch, + "do not watch for file changes", + ) + + fs.BoolVar( + &c.noReplay, + "no-replay", + defaultDevOptions.noReplay, + "do not replay previous transactions upon reload", + ) + + fs.Int64Var( + &c.maxGas, + "max-gas", + defaultDevOptions.maxGas, + "set the maximum gas per block", + ) +} + +func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { + ctx, cancel := context.WithCancelCause(context.Background()) + defer cancel(nil) + + // Setup Raw Terminal + rt, restore, err := setupRawTerm(cfg, io) + if err != nil { + return fmt.Errorf("unable to init raw term: %w", err) + } + defer restore() + + // Setup trap signal + osm.TrapSignal(func() { + cancel(nil) + restore() + }) + + logger := setuplogger(cfg, rt) + loggerEvents := logger.WithGroup(EventServerLogName) + emitterServer := emitter.NewServer(loggerEvents) + + // load keybase + kb, err := setupKeybase(logger, cfg) + if err != nil { + return fmt.Errorf("unable to load keybase: %w", err) + } + + // Check and Parse packages + pkgpaths, err := resolvePackagesPathFromArgs(cfg, kb, args) + if err != nil { + return fmt.Errorf("unable to parse package paths: %w", err) + } + + // Setup Dev Node + // XXX: find a good way to export or display node logs + nodeLogger := logger.WithGroup(NodeLogName) + devNode, err := setupDevNode(ctx, nodeLogger, cfg, emitterServer, kb, pkgpaths) + if err != nil { + return err + } + defer devNode.Close() + + nodeLogger.Info("node started", "lisn", devNode.GetRemoteAddress(), "chainID", cfg.chainId) + + // Create server + mux := http.NewServeMux() + server := http.Server{ + Handler: mux, + Addr: cfg.webListenerAddr, + } + defer server.Close() + + // Setup gnoweb + webhandler := setupGnoWebServer(logger.WithGroup(WebLogName), cfg, devNode) + + // Setup HotReload if needed + if !cfg.noWatch { + evtstarget := fmt.Sprintf("%s/_events", server.Addr) + mux.Handle("/_events", emitterServer) + mux.Handle("/", emitter.NewMiddleware(evtstarget, webhandler)) + } else { + mux.Handle("/", webhandler) + } + + go func() { + err := server.ListenAndServe() + cancel(err) + }() + + logger.WithGroup(WebLogName). + Info("gnoweb started", + "lisn", fmt.Sprintf("http://%s", server.Addr)) + + watcher, err := watcher.NewPackageWatcher(loggerEvents, emitterServer) + if err != nil { + return fmt.Errorf("unable to setup packages watcher: %w", err) + } + defer watcher.Stop() + + // Add node pkgs to watcher + watcher.AddPackages(devNode.ListPkgs()...) + + if !cfg.serverMode { + logger.WithGroup("--- READY").Info("for commands and help, press `h`") + } + + // Run the main event loop + return runEventLoop(ctx, logger, kb, rt, devNode, watcher) +} + +var helper string = ` +A Accounts - Display known accounts +H Help - Display this message +R Reload - Reload all packages to take change into account. +Ctrl+R Reset - Reset application state. +Ctrl+C Exit - Exit the application +` + +func runEventLoop( + ctx context.Context, + logger *slog.Logger, + kb keys.Keybase, + rt *rawterm.RawTerm, + dnode *dev.Node, + watch *watcher.PackageWatcher, +) error { + + keyPressCh := listenForKeyPress(logger.WithGroup(KeyPressLogName), rt) + for { + var err error + + select { + case <-ctx.Done(): + return context.Cause(ctx) + case pkgs, ok := <-watch.PackagesUpdate: + if !ok { + return nil + } + + // fmt.Fprintln(nodeOut, "Loading package updates...") + if err = dnode.UpdatePackages(pkgs.PackagesPath()...); err != nil { + return fmt.Errorf("unable to update packages: %w", err) + } + + logger.WithGroup(NodeLogName).Info("reloading...") + if err = dnode.Reload(ctx); err != nil { + logger.WithGroup(NodeLogName). + Error("unable to reload node", "err", err) + } + + case key, ok := <-keyPressCh: + if !ok { + return nil + } + + logger.WithGroup(KeyPressLogName).Debug( + fmt.Sprintf("<%s>", key.String()), + ) + + switch key.Upper() { + case rawterm.KeyH: // Helper + logger.Info("Gno Dev Helper", "helper", helper) + case rawterm.KeyA: // Accounts + logAccounts(logger.WithGroup("accounts"), kb, dnode) + case rawterm.KeyR: // Reload + logger.WithGroup(NodeLogName).Info("reloading...") + if err = dnode.ReloadAll(ctx); err != nil { + logger.WithGroup(NodeLogName). + Error("unable to reload node", "err", err) + + } + + case rawterm.KeyCtrlR: // Reset + logger.WithGroup(NodeLogName).Info("reseting node state...") + if err = dnode.Reset(ctx); err != nil { + logger.WithGroup(NodeLogName). + Error("unable to reset node state", "err", err) + } + + case rawterm.KeyCtrlC: // Exit + return nil + default: + } + + // Reset listen for the next keypress + keyPressCh = listenForKeyPress(logger.WithGroup(KeyPressLogName), rt) + } + } +} + +func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm.KeyPress { + cc := make(chan rawterm.KeyPress, 1) + go func() { + defer close(cc) + key, err := rt.ReadKeyPress() + if err != nil { + logger.Error("unable to read keypress", "err", err) + return + } + + cc <- key + }() + + return cc +} + +func resolvePackagesPathFromArgs(cfg *devCfg, kb keys.Keybase, args []string) ([]dev.PackagePath, error) { + paths := make([]dev.PackagePath, len(args)) + + if cfg.genesisCreator == "" { + return nil, fmt.Errorf("default genesis creator cannot be empty") + } + + defaultKey, err := kb.GetByNameOrAddress(cfg.genesisCreator) + if err != nil { + return nil, fmt.Errorf("unable to get genesis creator %q: %w", cfg.genesisCreator, err) + } + + for i, arg := range args { + path, err := dev.ResolvePackagePathQuery(kb, arg) + if err != nil { + return nil, fmt.Errorf("invalid package path/query %q: %w", arg, err) + } + + // Assign a default creator if user haven't specified it. + if path.Creator.IsZero() { + path.Creator = defaultKey.GetAddress() + } + + paths[i] = path + } + + // Add examples folder if minimal is set to false + if !cfg.minimal { + paths = append(paths, gnodev.PackagePath{ + Path: filepath.Join(cfg.root, "examples"), + Creator: defaultKey.GetAddress(), + Deposit: nil, + }) + } + + return paths, nil +} diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go new file mode 100644 index 00000000000..dde94c843d2 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -0,0 +1,73 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "net" + "strings" + + gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" + "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" +) + +// setupDevNode initializes and returns a new DevNode. +func setupDevNode( + ctx context.Context, + logger *slog.Logger, + cfg *devCfg, + remitter emitter.Emitter, + kb keys.Keybase, + pkgspath []gnodev.PackagePath, +) (*gnodev.Node, error) { + balances, err := generateBalances(kb, cfg) + if err != nil { + return nil, fmt.Errorf("unable to generate balances: %w", err) + } + logger.Debug("balances loaded", "list", balances.List()) + + config := setupDevNodeConfig(cfg, balances, pkgspath) + return gnodev.NewDevNode(ctx, logger, remitter, config) +} + +// setupDevNodeConfig creates and returns a new dev.NodeConfig. +func setupDevNodeConfig(cfg *devCfg, balances gnoland.Balances, pkgspath []gnodev.PackagePath) *gnodev.NodeConfig { + config := gnodev.DefaultNodeConfig(cfg.root) + config.BalancesList = balances.List() + config.PackagesPathList = pkgspath + config.TMConfig.RPC.ListenAddress = resolveUnixOrTCPAddr(cfg.nodeRPCListenerAddr) + config.NoReplay = cfg.noReplay + config.MaxGasPerBlock = cfg.maxGas + config.ChainID = cfg.chainId + + // other listeners + config.TMConfig.P2P.ListenAddress = defaultDevOptions.nodeP2PListenerAddr + config.TMConfig.ProxyApp = defaultDevOptions.nodeProxyAppListenerAddr + + return config +} + +func resolveUnixOrTCPAddr(in string) (out string) { + var err error + var addr net.Addr + + if strings.HasPrefix(in, "unix://") { + in = strings.TrimPrefix(in, "unix://") + if addr, err := net.ResolveUnixAddr("unix", in); err == nil { + return fmt.Sprintf("%s://%s", addr.Network(), addr.String()) + } + + err = fmt.Errorf("unable to resolve unix address `unix://%s`: %w", in, err) + } else { // don't bother to checking prefix + in = strings.TrimPrefix(in, "tcp://") + if addr, err = net.ResolveTCPAddr("tcp", in); err == nil { + return fmt.Sprintf("%s://%s", addr.Network(), addr.String()) + } + + err = fmt.Errorf("unable to resolve tcp address `tcp://%s`: %w", in, err) + } + + panic(err) +} diff --git a/contribs/gnodev/cmd/gnodev/setup_term.go b/contribs/gnodev/cmd/gnodev/setup_term.go new file mode 100644 index 00000000000..1f8f3046969 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/setup_term.go @@ -0,0 +1,24 @@ +package main + +import ( + "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +var noopRestore = func() error { return nil } + +func setupRawTerm(cfg *devCfg, io commands.IO) (*rawterm.RawTerm, func() error, error) { + rt := rawterm.NewRawTerm() + restore := noopRestore + if !cfg.serverMode { + var err error + restore, err = rt.Init() + if err != nil { + return nil, nil, err + } + } + + // correctly format output for terminal + io.SetOut(commands.WriteNopCloser(rt)) + return rt, restore, nil +} diff --git a/contribs/gnodev/cmd/gnodev/setup_web.go b/contribs/gnodev/cmd/gnodev/setup_web.go new file mode 100644 index 00000000000..17df502c3d8 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/setup_web.go @@ -0,0 +1,20 @@ +package main + +import ( + "log/slog" + "net/http" + + gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" + "github.com/gnolang/gno/gno.land/pkg/gnoweb" +) + +// setupGnowebServer initializes and starts the Gnoweb server. +func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) http.Handler { + webConfig := gnoweb.NewDefaultConfig() + webConfig.RemoteAddr = dnode.GetRemoteAddress() + webConfig.HelpRemote = dnode.GetRemoteAddress() + webConfig.HelpChainID = cfg.chainId + + app := gnoweb.MakeApp(logger, webConfig) + return app.Router +} diff --git a/contribs/gnodev/main.go b/contribs/gnodev/main.go deleted file mode 100644 index f024f4e0bae..00000000000 --- a/contribs/gnodev/main.go +++ /dev/null @@ -1,738 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "io" - "log/slog" - "net" - "net/http" - "os" - "path/filepath" - "strings" - "time" - - "github.com/charmbracelet/lipgloss" - "github.com/fsnotify/fsnotify" - "github.com/gnolang/gno/contribs/gnodev/pkg/dev" - gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" - "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" - "github.com/gnolang/gno/contribs/gnodev/pkg/logger" - "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" - "github.com/gnolang/gno/contribs/gnodev/pkg/watcher" - "github.com/gnolang/gno/gno.land/pkg/balances" - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/gno.land/pkg/gnoweb" - "github.com/gnolang/gno/gno.land/pkg/integration" - gnolog "github.com/gnolang/gno/gno.land/pkg/log" - "github.com/gnolang/gno/gnovm/pkg/gnoenv" - "github.com/gnolang/gno/gnovm/pkg/gnomod" - "github.com/gnolang/gno/tm2/pkg/amino" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/crypto/bip39" - "github.com/gnolang/gno/tm2/pkg/crypto/keys" - "github.com/gnolang/gno/tm2/pkg/crypto/keys/keyerror" - osm "github.com/gnolang/gno/tm2/pkg/os" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/muesli/termenv" -) - -const ( - NodeLogName = "Node" - WebLogName = "GnoWeb" - KeyPressLogName = "KeyPress" - EventServerLogName = "Event" -) - -var ( - DefaultCreatorName = integration.DefaultAccount_Name - DefaultCreatorAddress = crypto.MustAddressFromString(integration.DefaultAccount_Address) - DefaultCreatorSeed = integration.DefaultAccount_Seed - DefaultFee = std.NewFee(50000, std.MustParseCoin("1000000ugnot")) -) - -type devCfg struct { - // Listeners - webListenerAddr string - nodeRPCListenerAddr string - nodeP2PListenerAddr string - nodeProxyAppListenerAddr string - - // Users default - genesisCreator string - home string - root string - additionalUsers varAccounts - - // Node Configuration - minimal bool - verbose bool - hotreload bool - noWatch bool - noReplay bool - maxGas int64 - chainId string - serverMode bool - balancesFile string -} - -var defaultDevOptions = &devCfg{ - chainId: "dev", - maxGas: 10_000_000_000, - webListenerAddr: "127.0.0.1:8888", - nodeRPCListenerAddr: "127.0.0.1:36657", - genesisCreator: DefaultCreatorAddress.String(), - home: gnoenv.HomeDir(), - root: gnoenv.RootDir(), - - // As we have no reason to configure this yet, set this to random port - // to avoid potential conflict with other app - nodeP2PListenerAddr: "tcp://127.0.0.1:0", - nodeProxyAppListenerAddr: "tcp://127.0.0.1:0", -} - -func main() { - cfg := &devCfg{} - - stdio := commands.NewDefaultIO() - cmd := commands.NewCommand( - commands.Metadata{ - Name: "gnodev", - ShortUsage: "gnodev [flags] [path ...]", - ShortHelp: "runs an in-memory node and gno.land web server for development purposes.", - LongHelp: `The gnodev command starts an in-memory node and a gno.land web interface -primarily for realm package development. It automatically loads the 'examples' directory and any -additional specified paths.`, - }, - cfg, - func(_ context.Context, args []string) error { - return execDev(cfg, args, stdio) - }) - - cmd.Execute(context.Background(), os.Args[1:]) -} - -func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { - fs.StringVar( - &c.home, - "home", - defaultDevOptions.home, - "user's local directory for keys", - ) - - fs.StringVar( - &c.root, - "root", - defaultDevOptions.root, - "gno root directory", - ) - - fs.StringVar( - &c.webListenerAddr, - "web-listener", - defaultDevOptions.webListenerAddr, - "web server listening address", - ) - - fs.StringVar( - &c.nodeRPCListenerAddr, - "node-rpc-listener", - defaultDevOptions.nodeRPCListenerAddr, - "listening address for GnoLand RPC node", - ) - - fs.Var( - &c.additionalUsers, - "add-user", - "pre-add a user", - ) - - fs.StringVar( - &c.genesisCreator, - "genesis-creator", - defaultDevOptions.genesisCreator, - "name or bech32 address of the genesis creator", - ) - - fs.BoolVar( - &c.minimal, - "minimal", - defaultDevOptions.minimal, - "do not load packages from the examples directory", - ) - - fs.BoolVar( - &c.serverMode, - "server-mode", - defaultDevOptions.serverMode, - "disable interaction, and adjust logging for server use.", - ) - - fs.BoolVar( - &c.verbose, - "verbose", - defaultDevOptions.verbose, - "enable verbose output for development", - ) - - fs.StringVar( - &c.chainId, - "chain-id", - defaultDevOptions.chainId, - "set node ChainID", - ) - - fs.BoolVar( - &c.noWatch, - "no-watch", - defaultDevOptions.noWatch, - "do not watch for file changes", - ) - - fs.BoolVar( - &c.noReplay, - "no-replay", - defaultDevOptions.noReplay, - "do not replay previous transactions upon reload", - ) - - fs.Int64Var( - &c.maxGas, - "max-gas", - defaultDevOptions.maxGas, - "set the maximum gas per block", - ) -} - -func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { - ctx, cancel := context.WithCancelCause(context.Background()) - defer cancel(nil) - - // Setup Raw Terminal - rt, restore, err := setupRawTerm(cfg, io) - if err != nil { - return fmt.Errorf("unable to init raw term: %w", err) - } - defer restore() - - // Setup trap signal - osm.TrapSignal(func() { - cancel(nil) - restore() - }) - - logger := setuplogger(cfg, rt) - loggerEvents := logger.WithGroup(EventServerLogName) - emitterServer := emitter.NewServer(loggerEvents) - - // load keybase - kb, err := setupKeybase(cfg, logger) - if err != nil { - return fmt.Errorf("unable to load keybase: %w", err) - } - - // Check and Parse packages - pkgpaths, err := resolvePackagesPathFromArgs(cfg, kb, args) - if err != nil { - return fmt.Errorf("unable to parse package paths: %w", err) - } - - // Setup Dev Node - // XXX: find a good way to export or display node logs - nodeLogger := logger.WithGroup(NodeLogName) - devNode, err := setupDevNode(ctx, nodeLogger, cfg, emitterServer, kb, pkgpaths) - if err != nil { - return err - } - defer devNode.Close() - - nodeLogger.Info("node started", "lisn", devNode.GetRemoteAddress(), "chainID", cfg.chainId) - - // Create server - mux := http.NewServeMux() - server := http.Server{ - Handler: mux, - Addr: cfg.webListenerAddr, - } - defer server.Close() - - // Setup gnoweb - webhandler := setupGnoWebServer(logger.WithGroup(WebLogName), cfg, devNode) - - // Setup HotReload if needed - if !cfg.noWatch { - evtstarget := fmt.Sprintf("%s/_events", server.Addr) - mux.Handle("/_events", emitterServer) - mux.Handle("/", emitter.NewMiddleware(evtstarget, webhandler)) - } else { - mux.Handle("/", webhandler) - } - - go func() { - err := server.ListenAndServe() - cancel(err) - }() - - logger.WithGroup(WebLogName). - Info("gnoweb started", - "lisn", fmt.Sprintf("http://%s", server.Addr)) - - watcher, err := watcher.NewPackageWatcher(loggerEvents, emitterServer) - if err != nil { - return fmt.Errorf("unable to setup packages watcher: %w", err) - } - defer watcher.Stop() - - // Add node pkgs to watcher - watcher.AddPackages(devNode.ListPkgs()...) - - if !cfg.serverMode { - logger.WithGroup("--- READY").Info("for commands and help, press `h`") - } - - // Run the main event loop - return runEventLoop(ctx, logger, kb, rt, devNode, watcher) -} - -var helper string = ` -A Accounts - Display known accounts -H Help - Display this message -R Reload - Reload all packages to take change into account. -Ctrl+R Reset - Reset application state. -Ctrl+C Exit - Exit the application -` - -func runEventLoop( - ctx context.Context, - logger *slog.Logger, - kb keys.Keybase, - rt *rawterm.RawTerm, - dnode *dev.Node, - watch *watcher.PackageWatcher, -) error { - - keyPressCh := listenForKeyPress(logger.WithGroup(KeyPressLogName), rt) - for { - var err error - - select { - case <-ctx.Done(): - return context.Cause(ctx) - case pkgs, ok := <-watch.PackagesUpdate: - if !ok { - return nil - } - - // fmt.Fprintln(nodeOut, "Loading package updates...") - if err = dnode.UpdatePackages(pkgs.PackagesPath()...); err != nil { - return fmt.Errorf("unable to update packages: %w", err) - } - - logger.WithGroup(NodeLogName).Info("reloading...") - if err = dnode.Reload(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to reload node", "err", err) - } - - case key, ok := <-keyPressCh: - if !ok { - return nil - } - - logger.WithGroup(KeyPressLogName).Debug( - fmt.Sprintf("<%s>", key.String()), - ) - - switch key.Upper() { - case rawterm.KeyH: // Helper - logger.Info("Gno Dev Helper", "helper", helper) - case rawterm.KeyA: // Accounts - logAccounts(logger.WithGroup("accounts"), kb, dnode) - case rawterm.KeyR: // Reload - logger.WithGroup(NodeLogName).Info("reloading...") - if err = dnode.ReloadAll(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to reload node", "err", err) - - } - - case rawterm.KeyCtrlR: // Reset - logger.WithGroup(NodeLogName).Info("reseting node state...") - if err = dnode.Reset(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to reset node state", "err", err) - } - - case rawterm.KeyCtrlC: // Exit - return nil - default: - } - - // Reset listen for the next keypress - keyPressCh = listenForKeyPress(logger.WithGroup(KeyPressLogName), rt) - } - } -} - -func runPkgsWatcher(ctx context.Context, cfg *devCfg, pkgs []gnomod.Pkg, changedPathsCh chan<- []string) error { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return fmt.Errorf("unable to watch files: %w", err) - } - - if cfg.noWatch { - // Noop watcher, wait until context has been cancel - <-ctx.Done() - return ctx.Err() - } - - for _, pkg := range pkgs { - if err := watcher.Add(pkg.Dir); err != nil { - return fmt.Errorf("unable to watch %q: %w", pkg.Dir, err) - } - } - - const timeout = time.Millisecond * 500 - - var debounceTimer <-chan time.Time - pathList := []string{} - - for { - select { - case <-ctx.Done(): - return ctx.Err() - case watchErr := <-watcher.Errors: - return fmt.Errorf("watch error: %w", watchErr) - case <-debounceTimer: - changedPathsCh <- pathList - // Reset pathList and debounceTimer - pathList = []string{} - debounceTimer = nil - case evt := <-watcher.Events: - if evt.Op != fsnotify.Write { - continue - } - - pathList = append(pathList, evt.Name) - debounceTimer = time.After(timeout) - } - } -} - -var noopRestore = func() error { return nil } - -func setupRawTerm(cfg *devCfg, io commands.IO) (*rawterm.RawTerm, func() error, error) { - rt := rawterm.NewRawTerm() - restore := noopRestore - if !cfg.serverMode { - var err error - restore, err = rt.Init() - if err != nil { - return nil, nil, err - } - } - - // correctly format output for terminal - io.SetOut(commands.WriteNopCloser(rt)) - return rt, restore, nil -} - -// setupDevNode initializes and returns a new DevNode. -func setupDevNode( - ctx context.Context, - logger *slog.Logger, - cfg *devCfg, - remitter emitter.Emitter, - kb keys.Keybase, - pkgspath []dev.PackagePath, -) (*gnodev.Node, error) { - balances, err := generateBalances(kb, cfg) - if err != nil { - return nil, fmt.Errorf("unable to generate balances: %w", err) - } - logger.Debug("balances loaded", "list", balances.List()) - - // configure gnoland node - config := gnodev.DefaultNodeConfig(cfg.root) - config.BalancesList = balances.List() - config.PackagesPathList = pkgspath - config.TMConfig.RPC.ListenAddress = resolveUnixOrTCPAddr(cfg.nodeRPCListenerAddr) - config.NoReplay = cfg.noReplay - config.MaxGasPerBlock = cfg.maxGas - config.ChainID = cfg.chainId - - // other listeners - config.TMConfig.P2P.ListenAddress = defaultDevOptions.nodeP2PListenerAddr - config.TMConfig.ProxyApp = defaultDevOptions.nodeProxyAppListenerAddr - - return gnodev.NewDevNode(ctx, logger, remitter, config) -} - -// setupGnowebServer initializes and starts the Gnoweb server. -func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) http.Handler { - webConfig := gnoweb.NewDefaultConfig() - webConfig.RemoteAddr = dnode.GetRemoteAddress() - webConfig.HelpRemote = dnode.GetRemoteAddress() - webConfig.HelpChainID = cfg.chainId - - app := gnoweb.MakeApp(logger, webConfig) - return app.Router -} - -func resolvePackagesPathFromArgs(cfg *devCfg, kb keys.Keybase, args []string) ([]dev.PackagePath, error) { - paths := make([]dev.PackagePath, len(args)) - - if cfg.genesisCreator == "" { - return nil, fmt.Errorf("default genesis creator cannot be empty") - } - - defaultKey, err := kb.GetByNameOrAddress(cfg.genesisCreator) - if err != nil { - return nil, fmt.Errorf("unable to get genesis creator %q: %w", cfg.genesisCreator, err) - } - - for i, arg := range args { - path, err := dev.ResolvePackagePathQuery(kb, arg) - if err != nil { - return nil, fmt.Errorf("invalid package path/query %q: %w", arg, err) - } - - // Assign a default creator if user haven't specified it. - if path.Creator.IsZero() { - path.Creator = defaultKey.GetAddress() - } - - paths[i] = path - } - - // Add examples folder if minimal is set to false - if !cfg.minimal { - paths = append(paths, gnodev.PackagePath{ - Path: filepath.Join(cfg.root, "examples"), - Creator: defaultKey.GetAddress(), - Deposit: nil, - }) - } - - return paths, nil -} - -func generateBalances(kb keys.Keybase, cfg *devCfg) (balances.Balances, error) { - bls := balances.New() - unlimitedFund := std.Coins{std.NewCoin("ugnot", 10e12)} - - keys, err := kb.List() - if err != nil { - return nil, fmt.Errorf("unable to list keys from keybase: %w", err) - } - - // Automatically set every key from keybase to unlimited found (or pre - // defined found if specified) - for _, key := range keys { - found := unlimitedFund - if preDefinedFound, ok := cfg.additionalUsers[key.GetName()]; ok && preDefinedFound != nil { - found = preDefinedFound - } - - address := key.GetAddress() - bls[address] = gnoland.Balance{Amount: found, Address: address} - } - - if cfg.balancesFile == "" { - return bls, nil - } - - file, err := os.Open(cfg.balancesFile) - if err != nil { - return nil, fmt.Errorf("unable to open balance file %q: %w", cfg.balancesFile, err) - } - - blsFile, err := balances.GetBalancesFromSheet(file) - if err != nil { - return nil, fmt.Errorf("unable to read balances file %q: %w", cfg.balancesFile, err) - } - - // Left merge keybase balance into loaded file balance - blsFile.LeftMerge(bls) - return blsFile, nil -} - -func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm.KeyPress { - cc := make(chan rawterm.KeyPress, 1) - go func() { - defer close(cc) - key, err := rt.ReadKeyPress() - if err != nil { - logger.Error("unable to read keypress", "err", err) - return - } - - cc <- key - }() - - return cc -} - -// createAccount creates a new account with the given name and adds it to the keybase. -func createAccount(kb keys.Keybase, accountName string) (keys.Info, error) { - entropy, err := bip39.NewEntropy(256) - if err != nil { - return nil, fmt.Errorf("error creating entropy: %w", err) - } - - mnemonic, err := bip39.NewMnemonic(entropy) - if err != nil { - return nil, fmt.Errorf("error generating mnemonic: %w", err) - } - - return kb.CreateAccount(accountName, mnemonic, "", "", 0, 0) -} - -func resolveUnixOrTCPAddr(in string) (out string) { - var err error - var addr net.Addr - - if strings.HasPrefix(in, "unix://") { - in = strings.TrimPrefix(in, "unix://") - if addr, err := net.ResolveUnixAddr("unix", in); err == nil { - return fmt.Sprintf("%s://%s", addr.Network(), addr.String()) - } - - err = fmt.Errorf("unable to resolve unix address `unix://%s`: %w", in, err) - } else { // don't bother to checking prefix - in = strings.TrimPrefix(in, "tcp://") - if addr, err = net.ResolveTCPAddr("tcp", in); err == nil { - return fmt.Sprintf("%s://%s", addr.Network(), addr.String()) - } - - err = fmt.Errorf("unable to resolve tcp address `tcp://%s`: %w", in, err) - } - - panic(err) -} - -func setupKeybase(cfg *devCfg, logger *slog.Logger) (keys.Keybase, error) { - kb := keys.NewInMemory() - if cfg.home != "" { - // load home keybase into our inMemory keybase - kbHome, err := keys.NewKeyBaseFromDir(cfg.home) - if err != nil { - return nil, fmt.Errorf("unable to load keybae from dir %q: %w", cfg.home, err) - } - - keys, err := kbHome.List() - if err != nil { - return nil, fmt.Errorf("unable to list keys from keybase %q: %w", cfg.home, err) - } - - for _, key := range keys { - name := key.GetName() - armor, err := kbHome.Export(key.GetName()) - if err != nil { - return nil, fmt.Errorf("unable to export key %q: %w", name, err) - } - - if err := kb.Import(name, armor); err != nil { - return nil, fmt.Errorf("unable to import key %q: %w", name, err) - } - } - } - - // Add additional users to our keybase - for user := range cfg.additionalUsers { - info, err := createAccount(kb, user) - if err != nil { - return nil, fmt.Errorf("unable to create user %q: %w", user, err) - } - - logger.Info("additional user", "name", info.GetName(), "addr", info.GetAddress()) - } - - // Next, make sure that we have a default address to load packages - var info keys.Info - var err error - - info, err = kb.GetByNameOrAddress(cfg.genesisCreator) - switch { - case err == nil: // user already have a default user - case keyerror.IsErrKeyNotFound(err): - // if the key isn't found, create a default one - creatorName := fmt.Sprintf("_default#%.10s", DefaultCreatorAddress.String()) - if ok, _ := kb.HasByName(creatorName); ok { - // if a collision happen here, someone really want to not run. - return nil, fmt.Errorf("unable to create creator account, delete %q from your keybase", creatorName) - } - - info, err = kb.CreateAccount(creatorName, DefaultCreatorSeed, "", "", 0, 0) - if err != nil { - return nil, fmt.Errorf("unable to create default %q account: %w", DefaultCreatorName, err) - } - default: - return nil, fmt.Errorf("unable to get address %q from keybase: %w", info.GetAddress(), err) - } - - logger.Info("default creator", "name", info.GetName(), "addr", info.GetAddress()) - return kb, nil -} - -func logAccounts(logger *slog.Logger, kb keys.Keybase, _ *dev.Node) error { - keys, err := kb.List() - if err != nil { - return fmt.Errorf("unable to get keybase keys list: %w", err) - } - - accounts := make([]string, len(keys)) - for i, key := range keys { - if key.GetName() == "" { - continue // skip empty key name - } - - address := key.GetAddress() - qres, err := client.NewLocal().ABCIQuery("auth/accounts/"+address.String(), []byte{}) - if err != nil { - return fmt.Errorf("unable to querry account %q: %w", address.String(), err) - } - - var qret struct{ BaseAccount std.BaseAccount } - if err = amino.UnmarshalJSON(qres.Response.Data, &qret); err != nil { - return fmt.Errorf("unable to unmarshal query response: %w", err) - } - - // format name - (address) -> (coins) -> (acct-num) -> (seq) - accounts[i] = fmt.Sprintf("%s: addr(%s) coins(%s) acct_num(%d)", - key.GetName(), - address.String(), - qret.BaseAccount.GetCoins().String(), - qret.BaseAccount.GetAccountNumber()) - } - - logger.Info("current accounts", "balances", strings.Join(accounts, "\n")) - return nil -} - -func setuplogger(cfg *devCfg, out io.Writer) *slog.Logger { - level := slog.LevelInfo - if cfg.verbose { - level = slog.LevelDebug - } - - if cfg.serverMode { - zaplogger := logger.NewZapLogger(out, level) - return gnolog.ZapLoggerToSlog(zaplogger) - } - - // Detect term color profile - colorProfile := termenv.DefaultOutput().Profile - clogger := logger.NewColumnLogger(out, level, colorProfile) - - // Register well known group color with system colors - clogger.RegisterGroupColor(NodeLogName, lipgloss.Color("3")) - clogger.RegisterGroupColor(WebLogName, lipgloss.Color("4")) - clogger.RegisterGroupColor(KeyPressLogName, lipgloss.Color("5")) - clogger.RegisterGroupColor(EventServerLogName, lipgloss.Color("6")) - - return slog.New(clogger) -} diff --git a/docs/gno-tooling/cli/gnodev.md b/docs/gno-tooling/cli/gnodev.md index fae635ed5b7..31dd4112dce 100644 --- a/docs/gno-tooling/cli/gnodev.md +++ b/docs/gno-tooling/cli/gnodev.md @@ -12,13 +12,15 @@ local instance of `gnoweb`, allowing you to see the rendering of your Gno code i ## Features - **In-Memory Node**: Gnodev starts an in-memory node, and automatically loads -the **examples** folder and any user-specified paths. + the **examples** folder and any user-specified paths. - **Web Interface Server**: Gnodev automatically starts a `gnoweb` server on [`localhost:8888`](https://localhost:8888). -- **Hot Reload**: Gnodev monitors the **examples** folder and any specified for file changes, -reloading and automatically restarting the node as needed. +- **Balances and Keybase Customization**: Users can specify balances, load from + a balances file or create temporary users. +- **Hot Reload**: Gnodev monitors the **examples** folder and any specified for + file changes, reloading and automatically restarting the node as needed. - **State Maintenance**: Gnodev replays all transactions in between reloads, -ensuring the previous node state is preserved. + ensuring the previous node state is preserved. ## Installation Gnodev can be found in the `contribs` folder in the monorepo. @@ -35,15 +37,45 @@ For hot reloading, `gnodev` watches the examples folder, as well as any specifie gnodev ./myrealm ``` +## Keybase and Balance + +Gnodev will by default, load your keybase located in GNOHOME directory. +Given all you keys an (almost unlimited found). + +All realm will be upload by the `-genesis-creator`, but You can also pass query +options to realm path load a realm with specific creator and deposit: +``` +gnodev ./myrealm?creator=foo&deposit=42ugnot`` +``` + +### Additional User +Use `-add-user` flag in the format (:) to add temporary users. You can repeat this to add multiple users. +Addresses of those will be display at runtime, or by pressing `A` interactivly to display accounts. + +## Interactive Usage + While `gnodev` is running, the following shortcuts are available: +- To see help, press `H`. +- To display accounts, press `A`. - To reload manually, press `R`. - To reset the state of the node, press `CMD+R`. -- To see help, press `H`. - To stop `gnodev`, press `CMD+C`. ### Options -| Flag | Effect | -|------------|-----------------------------------------------------| -| --minimal | Start `gnodev` without loading the examples folder. | -| --no-watch | Disable hot reload. | \ No newline at end of file +| Flag | Effect | +|---------------------|---------------------------------------------------------| +| --minimal | Start `gnodev` without loading the examples folder. | +| --no-watch | Disable hot reload. | +| --add-user | Pre-add user(s) in the form (:) | +| --balances-file | Load a balance for the user(s) from a balance file. | +| --chain-id | Set node ChainID | +| --genesis-creator | Name or bech32 address of the genesis creator | +| --home | Set the path to load user's Keybase. | +| --max-gas | Set the maximum gas per block | +| --no-replay | Do not replay previous transactions upon reload | +| --node-rpc-listener | listening address for GnoLand RPC node | +| --root | gno root directory | +| --server-mode | disable interaction, and adjust logging for server use. | +| --verbose | enable verbose output for development | +| --web-listener | web server listening address | diff --git a/gno.land/pkg/balances/balances.go b/gno.land/pkg/balances/balances.go deleted file mode 100644 index 0d6f313b00b..00000000000 --- a/gno.land/pkg/balances/balances.go +++ /dev/null @@ -1,100 +0,0 @@ -package balances - -import ( - "bufio" - "fmt" - "io" - "strings" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/std" -) - -type Balances map[crypto.Address]gnoland.Balance - -func New() Balances { - return make(Balances) -} - -func (balances Balances) Set(address crypto.Address, amount std.Coins) { - balances[address] = gnoland.Balance{ - Address: address, - Amount: amount, - } -} - -func (balances Balances) Get(address crypto.Address) (balance gnoland.Balance, ok bool) { - balance, ok = balances[address] - return -} - -func (balances Balances) List() []gnoland.Balance { - list := make([]gnoland.Balance, 0, len(balances)) - for _, balance := range balances { - list = append(list, balance) - } - return list -} - -// leftMerge left-merges the two maps -func (a Balances) LeftMerge(b Balances) { - for key, bVal := range b { - if _, present := (a)[key]; !present { - (a)[key] = bVal - } - } -} - -func GetBalancesFromEntries(entries ...string) (Balances, error) { - balances := New() - return balances, balances.LoadFromEntries(entries...) -} - -// LoadFromEntries extracts the balance entries in the form of
= -func (balances Balances) LoadFromEntries(entries ...string) error { - for _, entry := range entries { - var balance gnoland.Balance - if err := balance.Parse(entry); err != nil { - return fmt.Errorf("unable to parse balance entry: %w", err) - } - balances[balance.Address] = balance - } - - return nil -} - -func GetBalancesFromSheet(sheet io.Reader) (Balances, error) { - balances := New() - return balances, balances.LoadFromSheet(sheet) -} - -// LoadFromSheet extracts the balance sheet from the passed in -// balance sheet file, that has the format of
=ugnot -func (balances Balances) LoadFromSheet(sheet io.Reader) error { - // Parse the balances - scanner := bufio.NewScanner(sheet) - - for scanner.Scan() { - entry := scanner.Text() - - // Remove comments - entry = strings.Split(entry, "#")[0] - entry = strings.TrimSpace(entry) - - // Skip empty lines - if entry == "" { - continue - } - - if err := balances.LoadFromEntries(entry); err != nil { - return fmt.Errorf("unable to load entries: %w", err) - } - } - - if err := scanner.Err(); err != nil { - return fmt.Errorf("error encountered while scanning, %w", err) - } - - return nil -} diff --git a/gno.land/pkg/balances/balances_test.go b/gno.land/pkg/balances/balances_test.go deleted file mode 100644 index 7d42f4ea56c..00000000000 --- a/gno.land/pkg/balances/balances_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package balances - -import ( - "fmt" - "math" - "strconv" - "strings" - "testing" - - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/crypto/bip39" - "github.com/gnolang/gno/tm2/pkg/crypto/hd" - "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" - "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/jaekwon/testify/require" - "github.com/stretchr/testify/assert" -) - -func TestBalances_GetBalancesFromEntries(t *testing.T) { - t.Parallel() - - t.Run("valid balances", func(t *testing.T) { - t.Parallel() - - // Generate dummy keys - dummyKeys := getDummyKeys(t, 2) - amount := std.NewCoins(std.NewCoin("ugnot", 10)) - - entries := make([]string, len(dummyKeys)) - - for index, key := range dummyKeys { - entries[index] = fmt.Sprintf( - "%s=%dugnot", - key.Address().String(), - amount.AmountOf("ugnot"), - ) - } - - balanceMap, err := GetBalancesFromEntries(entries...) - require.NoError(t, err) - - // Validate the balance map - assert.Len(t, balanceMap, len(dummyKeys)) - for _, key := range dummyKeys { - assert.Equal(t, amount, balanceMap[key.Address()].Amount) - } - }) - - t.Run("malformed balance, invalid format", func(t *testing.T) { - t.Parallel() - - entries := []string{ - "malformed balance", - } - - balanceMap, err := GetBalancesFromEntries(entries...) - - assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, "malformed entry") - }) - - t.Run("malformed balance, invalid address", func(t *testing.T) { - t.Parallel() - - balances := []string{ - "dummyaddress=10ugnot", - } - - balanceMap, err := GetBalancesFromEntries(balances...) - - assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, "invalid address") - }) - - t.Run("malformed balance, invalid amount", func(t *testing.T) { - t.Parallel() - - dummyKey := getDummyKey(t) - - balances := []string{ - fmt.Sprintf( - "%s=%sugnot", - dummyKey.Address().String(), - strconv.FormatUint(math.MaxUint64, 10), - ), - } - - balanceMap, err := GetBalancesFromEntries(balances...) - - assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, "invalid amount") - }) -} - -func TestBalances_GetBalancesFromSheet(t *testing.T) { - t.Parallel() - - t.Run("valid balances", func(t *testing.T) { - t.Parallel() - - // Generate dummy keys - dummyKeys := getDummyKeys(t, 2) - amount := std.NewCoins(std.NewCoin("ugnot", 10)) - - balances := make([]string, len(dummyKeys)) - - for index, key := range dummyKeys { - balances[index] = fmt.Sprintf( - "%s=%dugnot", - key.Address().String(), - amount.AmountOf("ugnot"), - ) - } - - reader := strings.NewReader(strings.Join(balances, "\n")) - balanceMap, err := GetBalancesFromSheet(reader) - require.NoError(t, err) - - // Validate the balance map - assert.Len(t, balanceMap, len(dummyKeys)) - for _, key := range dummyKeys { - assert.Equal(t, amount, balanceMap[key.Address()].Amount) - } - }) - - t.Run("malformed balance, invalid amount", func(t *testing.T) { - t.Parallel() - - dummyKey := getDummyKey(t) - - balances := []string{ - fmt.Sprintf( - "%s=%sugnot", - dummyKey.Address().String(), - strconv.FormatUint(math.MaxUint64, 10), - ), - } - - reader := strings.NewReader(strings.Join(balances, "\n")) - - balanceMap, err := GetBalancesFromSheet(reader) - - assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, "invalid amount") - }) -} - -// 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 { - t.Helper() - - mnemonic, err := client.GenerateMnemonic(256) - require.NoError(t, err) - - seed := bip39.NewSeed(mnemonic, "") - - return generateKeyFromSeed(seed, 0).PubKey() -} - -// getDummyKeys generates random keys for testing -func getDummyKeys(t *testing.T, count int) []crypto.PubKey { - t.Helper() - - dummyKeys := make([]crypto.PubKey, count) - - for i := 0; i < count; i++ { - dummyKeys[i] = getDummyKey(t) - } - - return dummyKeys -} - -// generateKeyFromSeed generates a private key from -// the provided seed and index -func generateKeyFromSeed(seed []byte, index uint32) crypto.PrivKey { - pathParams := hd.NewFundraiserParams(0, crypto.CoinType, index) - - masterPriv, ch := hd.ComputeMastersFromSeed(seed) - - //nolint:errcheck // This derivation can never error out, since the path params - // are always going to be valid - derivedPriv, _ := hd.DerivePrivateKeyForPath(masterPriv, ch, pathParams.String()) - - return secp256k1.PrivKeySecp256k1(derivedPriv) -} diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index da6bef97746..d0ac52b40b0 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -82,7 +82,7 @@ func (b Balance) String() string { type Balances map[crypto.Address]Balance -func New() Balances { +func NewBalances() Balances { return make(Balances) } @@ -116,7 +116,7 @@ func (a Balances) LeftMerge(b Balances) { } func GetBalancesFromEntries(entries ...string) (Balances, error) { - balances := New() + balances := NewBalances() return balances, balances.LoadFromEntries(entries...) } @@ -134,7 +134,7 @@ func (balances Balances) LoadFromEntries(entries ...string) error { } func GetBalancesFromSheet(sheet io.Reader) (Balances, error) { - balances := New() + balances := NewBalances() return balances, balances.LoadFromSheet(sheet) } From 5bd0b6fd2ee971e3f13e735046b60646b77b95d0 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:25:06 +0200 Subject: [PATCH 04/23] fix: update docs Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- docs/gno-tooling/cli/gnodev.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/gno-tooling/cli/gnodev.md b/docs/gno-tooling/cli/gnodev.md index 31dd4112dce..a0b739e6df6 100644 --- a/docs/gno-tooling/cli/gnodev.md +++ b/docs/gno-tooling/cli/gnodev.md @@ -39,18 +39,18 @@ gnodev ./myrealm ## Keybase and Balance -Gnodev will by default, load your keybase located in GNOHOME directory. -Given all you keys an (almost unlimited found). +Gnodev will, by default, load your keybase located in the GNOHOME directory, giving all your keys (almost) unlimited fund. -All realm will be upload by the `-genesis-creator`, but You can also pass query -options to realm path load a realm with specific creator and deposit: +All realms will be by the `-genesis-creator`, but You can also pass query options to the realm path to load a +realm with specific creator and deposit: ``` gnodev ./myrealm?creator=foo&deposit=42ugnot`` ``` ### Additional User -Use `-add-user` flag in the format (:) to add temporary users. You can repeat this to add multiple users. -Addresses of those will be display at runtime, or by pressing `A` interactivly to display accounts. +Use `-add-user` flag in the format (:) to add temporary users. You can repeat this to add +multiple users. Addresses of those will be displayed at runtime, or by pressing `A` interactively to display +accounts. ## Interactive Usage From 446fd2f7816f15be4b01ec586f82a2eddd77c12d Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:50:41 +0200 Subject: [PATCH 05/23] fix: balances errors Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/cmd/genesis/balances_add.go | 17 ++++++++--------- gno.land/cmd/genesis/balances_remove.go | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/gno.land/cmd/genesis/balances_add.go b/gno.land/cmd/genesis/balances_add.go index 46fde3a7867..1497f4f6065 100644 --- a/gno.land/cmd/genesis/balances_add.go +++ b/gno.land/cmd/genesis/balances_add.go @@ -9,7 +9,6 @@ import ( "io" "os" - "github.com/gnolang/gno/gno.land/pkg/balances" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/types" @@ -93,11 +92,11 @@ func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io commands.IO) e return errNoBalanceSource } - finalBalances := balances.New() + finalBalances := gnoland.NewBalances() // Get the balance sheet from the source if singleEntriesSet { - balances, err := balances.GetBalancesFromEntries(cfg.singleEntries...) + balances, err := gnoland.GetBalancesFromEntries(cfg.singleEntries...) if err != nil { return fmt.Errorf("unable to get balances from entries, %w", err) } @@ -112,7 +111,7 @@ func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io commands.IO) e return fmt.Errorf("unable to open balance sheet, %w", loadErr) } - balances, err := balances.GetBalancesFromSheet(file) + balances, err := gnoland.GetBalancesFromSheet(file) if err != nil { return fmt.Errorf("unable to get balances from balance sheet, %w", err) } @@ -184,8 +183,8 @@ func getBalancesFromTransactions( ctx context.Context, io commands.IO, reader io.Reader, -) (balances.Balances, error) { - balances := balances.New() +) (gnoland.Balances, error) { + balances := gnoland.NewBalances() scanner := bufio.NewScanner(reader) @@ -235,7 +234,7 @@ func getBalancesFromTransactions( // This way of determining final account balances is not really valid, // because we take into account only the ugnot transfer messages (MsgSend) // and not other message types (like MsgCall), that can also - // initialize accounts with some balances. Because of this, + // initialize accounts with some gnoland. Because of this, // we can run into a situation where a message send amount or fee // causes an accounts balance to go < 0. In these cases, // we initialize the account (it is present in the balance sheet), but @@ -286,9 +285,9 @@ func getBalancesFromTransactions( // mapGenesisBalancesFromState extracts the initial account balances from the // genesis app state -func mapGenesisBalancesFromState(state gnoland.GnoGenesisState) (balances.Balances, error) { +func mapGenesisBalancesFromState(state gnoland.GnoGenesisState) (gnoland.Balances, error) { // Construct the initial genesis balance sheet - genesisBalances := balances.New() + genesisBalances := gnoland.NewBalances() for _, balance := range state.Balances { genesisBalances[balance.Address] = balance diff --git a/gno.land/cmd/genesis/balances_remove.go b/gno.land/cmd/genesis/balances_remove.go index a752bbda4fd..5b6e74e0dcf 100644 --- a/gno.land/cmd/genesis/balances_remove.go +++ b/gno.land/cmd/genesis/balances_remove.go @@ -86,7 +86,7 @@ func execBalancesRemove(cfg *balancesRemoveCfg, io commands.IO) error { delete(genesisBalances, address) // Save the balances - state.Balances = genesisBalances.toList() + state.Balances = genesisBalances.List() genesis.AppState = state // Save the updated genesis From 80e4f991ffa3fb326eeecce9762fa2334fa14c2b Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 17 Apr 2024 08:47:26 +0200 Subject: [PATCH 06/23] fix: lint and remove unused Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/Makefile | 7 +- contribs/gnodev/cmd/gnodev/main.go | 22 ++--- contribs/gnodev/pkg/dev/node.go | 107 +++++++++++----------- contribs/gnodev/pkg/emitter/middleware.go | 1 - contribs/gnodev/pkg/logger/colors.go | 2 +- contribs/gnodev/pkg/logger/log_column.go | 2 +- contribs/gnodev/pkg/watcher/watch.go | 2 +- tm2/pkg/crypto/keys/client/export.go | 7 ++ 8 files changed, 79 insertions(+), 71 deletions(-) diff --git a/contribs/gnodev/Makefile b/contribs/gnodev/Makefile index 7395b166a3e..b98ce0fb44b 100644 --- a/contribs/gnodev/Makefile +++ b/contribs/gnodev/Makefile @@ -1,9 +1,14 @@ GNOROOT_DIR ?= $(abspath $(lastword $(MAKEFILE_LIST))/../../../) +GOBUILD_FLAGS ?= -ldflags "-X github.com/gnolang/gno/gnovm/pkg/gnoenv._GNOROOT=$(GNOROOT_DIR)" -GOBUILD_FLAGS := -ldflags "-X github.com/gnolang/gno/gnovm/pkg/gnoenv._GNOROOT=$(GNOROOT_DIR)" +rundep := go run -modfile ../../misc/devdeps/go.mod +golangci_lint := $(rundep) github.com/golangci/golangci-lint/cmd/golangci-lint install: go install $(GOBUILD_FLAGS) ./cmd/gnodev build: go build $(GOBUILD_FLAGS) -o build/gnodev ./cmd/gnodev + +lint: + $(golangci_lint) --config ../../.github/golangci.yml run ./... diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 648fe78b20f..945f92c9ce6 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -8,8 +8,8 @@ import ( "net/http" "os" "path/filepath" + "time" - "github.com/gnolang/gno/contribs/gnodev/pkg/dev" gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" @@ -52,7 +52,6 @@ type devCfg struct { // Node Configuration minimal bool verbose bool - hotreload bool noWatch bool noReplay bool maxGas int64 @@ -235,8 +234,9 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { // Create server mux := http.NewServeMux() server := http.Server{ - Handler: mux, - Addr: cfg.webListenerAddr, + Handler: mux, + Addr: cfg.webListenerAddr, + ReadHeaderTimeout: time.Minute, } defer server.Close() @@ -291,10 +291,9 @@ func runEventLoop( logger *slog.Logger, kb keys.Keybase, rt *rawterm.RawTerm, - dnode *dev.Node, + dnode *gnodev.Node, watch *watcher.PackageWatcher, ) error { - keyPressCh := listenForKeyPress(logger.WithGroup(KeyPressLogName), rt) for { var err error @@ -337,7 +336,6 @@ func runEventLoop( if err = dnode.ReloadAll(ctx); err != nil { logger.WithGroup(NodeLogName). Error("unable to reload node", "err", err) - } case rawterm.KeyCtrlR: // Reset @@ -374,8 +372,8 @@ func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm. return cc } -func resolvePackagesPathFromArgs(cfg *devCfg, kb keys.Keybase, args []string) ([]dev.PackagePath, error) { - paths := make([]dev.PackagePath, len(args)) +func resolvePackagesPathFromArgs(cfg *devCfg, kb keys.Keybase, args []string) ([]gnodev.PackagePath, error) { + paths := make([]gnodev.PackagePath, 0, len(args)) if cfg.genesisCreator == "" { return nil, fmt.Errorf("default genesis creator cannot be empty") @@ -386,8 +384,8 @@ func resolvePackagesPathFromArgs(cfg *devCfg, kb keys.Keybase, args []string) ([ return nil, fmt.Errorf("unable to get genesis creator %q: %w", cfg.genesisCreator, err) } - for i, arg := range args { - path, err := dev.ResolvePackagePathQuery(kb, arg) + for _, arg := range args { + path, err := gnodev.ResolvePackagePathQuery(kb, arg) if err != nil { return nil, fmt.Errorf("invalid package path/query %q: %w", arg, err) } @@ -397,7 +395,7 @@ func resolvePackagesPathFromArgs(cfg *devCfg, kb keys.Keybase, args []string) ([ path.Creator = defaultKey.GetAddress() } - paths[i] = path + paths = append(paths, path) } // Add examples folder if minimal is set to false diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 786c9349d0e..971d3010bb1 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -112,39 +112,39 @@ func NewDevNode(ctx context.Context, logger *slog.Logger, emitter emitter.Emitte return devnode, nil } -func (d *Node) getLatestBlockNumber() uint64 { - return uint64(d.Node.BlockStore().Height()) +func (n *Node) getLatestBlockNumber() uint64 { + return uint64(n.Node.BlockStore().Height()) } -func (d *Node) Close() error { - return d.Node.Stop() +func (n *Node) Close() error { + return n.Node.Stop() } -func (d *Node) ListPkgs() []gnomod.Pkg { - return d.pkgs.toList() +func (n *Node) ListPkgs() []gnomod.Pkg { + return n.pkgs.toList() } -func (d *Node) GetNodeReadiness() <-chan struct{} { - return gnoland.GetNodeReadiness(d.Node) +func (n *Node) GetNodeReadiness() <-chan struct{} { + return gnoland.GetNodeReadiness(n.Node) } -func (d *Node) GetRemoteAddress() string { - return d.Node.Config().RPC.ListenAddress +func (n *Node) GetRemoteAddress() string { + return n.Node.Config().RPC.ListenAddress } // UpdatePackages updates the currently known packages. It will be taken into // consideration in the next reload of the node. -func (d *Node) UpdatePackages(paths ...string) error { - var n int +func (n *Node) UpdatePackages(paths ...string) error { + var i int for _, path := range paths { abspath, err := filepath.Abs(path) if err != nil { return fmt.Errorf("unable to resolve abs path of %q: %w", path, err) } - creator := d.config.DefaultCreator + creator := n.config.DefaultCreator var deposit std.Coins - for _, ppath := range d.config.PackagesPathList { + for _, ppath := range n.config.PackagesPathList { if !strings.HasPrefix(abspath, ppath.Path) { continue } @@ -161,117 +161,117 @@ func (d *Node) UpdatePackages(paths ...string) error { // Update or add package in the current known list. for _, pkg := range pkgslist { - d.pkgs[pkg.Dir] = Package{ + n.pkgs[pkg.Dir] = Package{ Pkg: pkg, Creator: creator, Deposit: deposit, } - d.logger.Debug("pkgs update", "name", pkg.Name, "path", pkg.Dir) + n.logger.Debug("pkgs update", "name", pkg.Name, "path", pkg.Dir) } - n += len(pkgslist) + i += len(pkgslist) } - d.logger.Info(fmt.Sprintf("updated %d pacakges", n)) + n.logger.Info(fmt.Sprintf("updated %d pacakges", i)) return nil } // Reset stops the node, if running, and reloads it with a new genesis state, // effectively ignoring the current state. -func (d *Node) Reset(ctx context.Context) error { +func (n *Node) Reset(ctx context.Context) error { // Stop the node if it's currently running. - if err := d.stopIfRunning(); err != nil { + if err := n.stopIfRunning(); err != nil { return fmt.Errorf("unable to stop the node: %w", err) } // Generate a new genesis state based on the current packages - txs, err := d.pkgs.Load(DefaultFee) + txs, err := n.pkgs.Load(DefaultFee) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } genesis := gnoland.GnoGenesisState{ - Balances: d.config.BalancesList, + Balances: n.config.BalancesList, Txs: txs, } // Reset the node with the new genesis state. - err = d.reset(ctx, genesis) + err = n.reset(ctx, genesis) if err != nil { return fmt.Errorf("unable to initialize a new node: %w", err) } - d.emitter.Emit(&events.Reset{}) + n.emitter.Emit(&events.Reset{}) return nil } // ReloadAll updates all currently known packages and then reloads the node. -func (d *Node) ReloadAll(ctx context.Context) error { - pkgs := d.ListPkgs() +func (n *Node) ReloadAll(ctx context.Context) error { + pkgs := n.ListPkgs() paths := make([]string, len(pkgs)) for i, pkg := range pkgs { paths[i] = pkg.Dir } - if err := d.UpdatePackages(paths...); err != nil { + if err := n.UpdatePackages(paths...); err != nil { return fmt.Errorf("unable to reload packages: %w", err) } - return d.Reload(ctx) + return n.Reload(ctx) } // Reload saves the current state, stops the node if running, starts a new node, // and re-apply previously saved state along with packages updated by `UpdatePackages`. // If any transaction, including 'addpkg', fails, it will be ignored. // Use 'Reset' to completely reset the node's state in case of persistent errors. -func (d *Node) Reload(ctx context.Context) error { - if d.config.NoReplay { +func (n *Node) Reload(ctx context.Context) error { + if n.config.NoReplay { // If NoReplay is true, reload as the same effect as reset - d.logger.Warn("replay disable") - return d.Reset(ctx) + n.logger.Warn("replay disable") + return n.Reset(ctx) } // Get current blockstore state - state, err := d.getBlockStoreState(ctx) + state, err := n.getBlockStoreState(ctx) if err != nil { return fmt.Errorf("unable to save state: %s", err.Error()) } // Stop the node if it's currently running. - if err := d.stopIfRunning(); err != nil { + if err := n.stopIfRunning(); err != nil { return fmt.Errorf("unable to stop the node: %w", err) } // Load genesis packages - pkgsTxs, err := d.pkgs.Load(DefaultFee) + pkgsTxs, err := n.pkgs.Load(DefaultFee) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } // Create genesis with loaded pkgs + previous state genesis := gnoland.GnoGenesisState{ - Balances: d.config.BalancesList, + Balances: n.config.BalancesList, Txs: append(pkgsTxs, state...), } // Reset the node with the new genesis state. - err = d.reset(ctx, genesis) - d.logger.Info("reload done", "pkgs", len(pkgsTxs), "state applied", len(state)) + err = n.reset(ctx, genesis) + n.logger.Info("reload done", "pkgs", len(pkgsTxs), "state applied", len(state)) // Update node infos - d.loadedPackages = len(pkgsTxs) + n.loadedPackages = len(pkgsTxs) - d.emitter.Emit(&events.Reload{}) + n.emitter.Emit(&events.Reload{}) return nil } -func (d *Node) genesisTxHandler(ctx sdk.Context, tx std.Tx, res sdk.Result) { +func (n *Node) genesisTxHandler(ctx sdk.Context, tx std.Tx, res sdk.Result) { if res.IsErr() { // XXX: for now, this is only way to catch the error before, after, found := strings.Cut(res.Log, "\n") if !found { - d.logger.Error("unable to send tx", "err", res.Error, "log", res.Log) + n.logger.Error("unable to send tx", "err", res.Error, "log", res.Log) return } @@ -287,20 +287,19 @@ func (d *Node) genesisTxHandler(ctx sdk.Context, tx std.Tx, res sdk.Result) { attrs = append(attrs, slog.String("err", msg)) // If debug is enable, also append stack - if d.logger.Enabled(context.Background(), slog.LevelDebug) { + if n.logger.Enabled(context.Background(), slog.LevelDebug) { attrs = append(attrs, slog.String("stack", after)) - } - d.logger.LogAttrs(context.Background(), slog.LevelError, "unable to deliver tx", attrs...) + n.logger.LogAttrs(context.Background(), slog.LevelError, "unable to deliver tx", attrs...) } } // GetBlockTransactions returns the transactions contained // within the specified block, if any -func (d *Node) GetBlockTransactions(blockNum uint64) ([]std.Tx, error) { +func (n *Node) GetBlockTransactions(blockNum uint64) ([]std.Tx, error) { int64BlockNum := int64(blockNum) - b, err := d.client.Block(&int64BlockNum) + 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 } @@ -320,38 +319,38 @@ func (d *Node) GetBlockTransactions(blockNum uint64) ([]std.Tx, error) { // GetBlockTransactions returns the transactions contained // within the specified block, if any -func (d *Node) CurrentBalances(blockNum uint64) ([]std.Tx, error) { +func (n *Node) CurrentBalances(blockNum uint64) ([]std.Tx, error) { return nil, nil } // GetBlockTransactions returns the transactions contained // within the specified block, if any // GetLatestBlockNumber returns the latest block height from the chain -func (d *Node) GetLatestBlockNumber() (uint64, error) { - return d.getLatestBlockNumber(), nil +func (n *Node) GetLatestBlockNumber() (uint64, error) { + return n.getLatestBlockNumber(), nil } // SendTransaction executes a broadcast commit send // of the specified transaction to the chain -func (d *Node) SendTransaction(tx *std.Tx) error { +func (n *Node) SendTransaction(tx *std.Tx) error { aminoTx, err := amino.Marshal(tx) if err != nil { return fmt.Errorf("unable to marshal transaction to amino binary, %w", err) } // we use BroadcastTxCommit to ensure to have one block with the given tx - res, err := d.client.BroadcastTxCommit(aminoTx) + res, err := n.client.BroadcastTxCommit(aminoTx) if err != nil { return fmt.Errorf("unable to broadcast transaction commit: %w", err) } if res.CheckTx.Error != nil { - d.logger.Error("check tx error trace", "log", res.CheckTx.Log) + n.logger.Error("check tx error trace", "log", res.CheckTx.Log) return fmt.Errorf("check transaction error: %w", res.CheckTx.Error) } if res.DeliverTx.Error != nil { - d.logger.Error("deliver tx error trace", "log", res.CheckTx.Log) + n.logger.Error("deliver tx error trace", "log", res.CheckTx.Log) return fmt.Errorf("deliver transaction error: %w", res.DeliverTx.Error) } diff --git a/contribs/gnodev/pkg/emitter/middleware.go b/contribs/gnodev/pkg/emitter/middleware.go index 80c07ec93aa..9c53cfe158e 100644 --- a/contribs/gnodev/pkg/emitter/middleware.go +++ b/contribs/gnodev/pkg/emitter/middleware.go @@ -89,7 +89,6 @@ func (m *middleware) ServeHTTP(rw http.ResponseWriter, req *http.Request) { events.EvtReload, events.EvtReset, events.EvtTxResult, }, }) - if err != nil { panic("unable to execute template: " + err.Error()) } diff --git a/contribs/gnodev/pkg/logger/colors.go b/contribs/gnodev/pkg/logger/colors.go index b0499e01722..1f6ec097488 100644 --- a/contribs/gnodev/pkg/logger/colors.go +++ b/contribs/gnodev/pkg/logger/colors.go @@ -11,7 +11,7 @@ import ( func colorFromString(s string, saturation, lightness float64) lipgloss.Color { hue := float64(hash32a(s) % 360) - r, g, b := hslToRGB(float64(hue), saturation, lightness) + r, g, b := hslToRGB(hue, saturation, lightness) hex := rgbToHex(r, g, b) return lipgloss.Color(hex) } diff --git a/contribs/gnodev/pkg/logger/log_column.go b/contribs/gnodev/pkg/logger/log_column.go index 8e145e89175..2a720525903 100644 --- a/contribs/gnodev/pkg/logger/log_column.go +++ b/contribs/gnodev/pkg/logger/log_column.go @@ -141,7 +141,7 @@ func (cl *columnWriter) Write(buf []byte) (n int, err error) { buf = buf[todo:] if cl.inline = i < 0; !cl.inline { - if _, err = cl.writer.Write([]byte(lf)); err != nil { + if _, err = cl.writer.Write(lf); err != nil { return n, err } n++ diff --git a/contribs/gnodev/pkg/watcher/watch.go b/contribs/gnodev/pkg/watcher/watch.go index a9ab189947f..63158a06c4b 100644 --- a/contribs/gnodev/pkg/watcher/watch.go +++ b/contribs/gnodev/pkg/watcher/watch.go @@ -61,7 +61,7 @@ func (p *PackageWatcher) startWatching() { defer close(pkgsUpdateChan) var debounceTimer <-chan time.Time - var pathList = []string{} + pathList := []string{} var err error for err == nil { diff --git a/tm2/pkg/crypto/keys/client/export.go b/tm2/pkg/crypto/keys/client/export.go index bb368342ef8..b80942734f6 100644 --- a/tm2/pkg/crypto/keys/client/export.go +++ b/tm2/pkg/crypto/keys/client/export.go @@ -99,6 +99,13 @@ func execExport(cfg *ExportCfg, io commands.IO) error { cfg.NameOrBech32, decryptPassword, ) + + privk, err := kb.ExportPrivateKeyObject(cfg.NameOrBech32, decryptPassword) + if err != nil { + panic(err) + } + + fmt.Printf("privk:\n%x\n", privk.Bytes()) } else { // Get the armor encrypt password encryptPassword, err := io.GetCheckPassword( From 93e4adc483f1b7a84d976300dd61809e9f41685f Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:31:33 +0200 Subject: [PATCH 07/23] chore: lint comment Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/accounts.go | 106 +++++-------------- contribs/gnodev/cmd/gnodev/main.go | 23 +++-- contribs/gnodev/cmd/gnodev/setup_keybase.go | 109 ++++++++++++++++++++ docs/gno-tooling/cli/gnodev.md | 15 ++- 4 files changed, 153 insertions(+), 100 deletions(-) create mode 100644 contribs/gnodev/cmd/gnodev/setup_keybase.go diff --git a/contribs/gnodev/cmd/gnodev/accounts.go b/contribs/gnodev/cmd/gnodev/accounts.go index 3335cd7fde9..0c40bf610cc 100644 --- a/contribs/gnodev/cmd/gnodev/accounts.go +++ b/contribs/gnodev/cmd/gnodev/accounts.go @@ -13,11 +13,10 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/gnolang/gno/tm2/pkg/crypto/bip39" "github.com/gnolang/gno/tm2/pkg/crypto/keys" - "github.com/gnolang/gno/tm2/pkg/crypto/keys/keyerror" "github.com/gnolang/gno/tm2/pkg/std" ) -type varAccounts map[string]std.Coins // name or bech32 -> coins +type varAccounts map[string]std.Coins // name or bech32 to coins. func (va *varAccounts) Set(value string) error { if *va == nil { @@ -36,7 +35,7 @@ func (va *varAccounts) Set(value string) error { return fmt.Errorf("unable to parse coins from %q: %w", user, err) } - // Add the parsed amount into user + // Add the parsed amount to the user. accounts[user] = coins return nil } @@ -50,67 +49,6 @@ func (va varAccounts) String() string { return strings.Join(accs, ",") } -func setupKeybase(logger *slog.Logger, cfg *devCfg) (keys.Keybase, error) { - kb := keys.NewInMemory() - if cfg.home != "" { - // Load home keybase into our inMemory keybase - kbHome, err := keys.NewKeyBaseFromDir(cfg.home) - if err != nil { - return nil, fmt.Errorf("unable to load keybase from dir %q: %w", cfg.home, err) - } - - keys, err := kbHome.List() - if err != nil { - return nil, fmt.Errorf("unable to list keys from keybase %q: %w", cfg.home, err) - } - - for _, key := range keys { - name := key.GetName() - armor, err := kbHome.Export(name) - if err != nil { - return nil, fmt.Errorf("unable to export key %q: %w", name, err) - } - - if err := kb.Import(name, armor); err != nil { - return nil, fmt.Errorf("unable to import key %q: %w", name, err) - } - } - } - - // Add additional users to our keybase - for user := range cfg.additionalUsers { - info, err := createAccount(kb, user) - if err != nil { - return nil, fmt.Errorf("unable to create user %q: %w", user, err) - } - - logger.Info("additional user", "name", info.GetName(), "addr", info.GetAddress()) - } - - // Next, make sure that we have a default address to load packages - info, err := kb.GetByNameOrAddress(cfg.genesisCreator) - switch { - case err == nil: // user already have a default user - break - case keyerror.IsErrKeyNotFound(err): - // If the key isn't found, create a default one - creatorName := fmt.Sprintf("_default#%.10s", DefaultCreatorAddress.String()) - if ok, _ := kb.HasByName(creatorName); ok { - return nil, fmt.Errorf("unable to create creator account, delete %q from your keybase", creatorName) - } - - info, err = kb.CreateAccount(creatorName, DefaultCreatorSeed, "", "", 0, 0) - if err != nil { - return nil, fmt.Errorf("unable to create default %q account: %w", DefaultCreatorName, err) - } - default: - return nil, fmt.Errorf("unable to get address %q from keybase: %w", info.GetAddress(), err) - } - - logger.Info("default creator", "name", info.GetName(), "addr", info.GetAddress()) - return kb, nil -} - func generateBalances(kb keys.Keybase, cfg *devCfg) (gnoland.Balances, error) { bls := gnoland.NewBalances() unlimitedFund := std.Coins{std.NewCoin("ugnot", 10e12)} @@ -120,15 +58,18 @@ func generateBalances(kb keys.Keybase, cfg *devCfg) (gnoland.Balances, error) { return nil, fmt.Errorf("unable to list keys from keybase: %w", err) } - // Automatically set every key from keybase to unlimited found (or pre - // defined found if specified) + // Automatically set every key from keybase to unlimited fund. for _, key := range keys { + address := key.GetAddress() + + // Check if a predefined amount has been set for this key. found := unlimitedFund - if preDefinedFound, ok := cfg.additionalUsers[key.GetName()]; ok && preDefinedFound != nil { + if preDefinedFound, ok := cfg.additionalAccounts[key.GetName()]; ok && preDefinedFound != nil { + found = preDefinedFound + } else if preDefinedFound, ok := cfg.additionalAccounts[address.String()]; ok && preDefinedFound != nil { found = preDefinedFound } - address := key.GetAddress() bls[address] = gnoland.Balance{Amount: found, Address: address} } @@ -146,7 +87,7 @@ func generateBalances(kb keys.Keybase, cfg *devCfg) (gnoland.Balances, error) { return nil, fmt.Errorf("unable to read balances file %q: %w", cfg.balancesFile, err) } - // Left merge keybase balance into loaded file balance + // Left merge keybase balance into loaded file balance. blsFile.LeftMerge(bls) return blsFile, nil } @@ -158,20 +99,18 @@ func logAccounts(logger *slog.Logger, kb keys.Keybase, _ *dev.Node) error { } var tab strings.Builder - tab.WriteRune('\n') tabw := tabwriter.NewWriter(&tab, 0, 0, 2, ' ', tabwriter.TabIndent) - fmt.Fprintln(tabw, "KeyName\tAddress\tBalance") // Table header + fmt.Fprintln(tabw, "KeyName\tAddress\tBalance") // Table header. for _, key := range keys { if key.GetName() == "" { - continue // skip empty key name + continue // Skip empty key name. } address := key.GetAddress() - // XXX: use client from node from argument, should be exposed by the node directly qres, err := client.NewLocal().ABCIQuery("auth/accounts/"+address.String(), []byte{}) if err != nil { - return fmt.Errorf("unable to querry account %q: %w", address.String(), err) + return fmt.Errorf("unable to query account %q: %w", address.String(), err) } var qret struct{ BaseAccount std.BaseAccount } @@ -179,12 +118,12 @@ func logAccounts(logger *slog.Logger, kb keys.Keybase, _ *dev.Node) error { return fmt.Errorf("unable to unmarshal query response: %w", err) } - // Insert row with name, addr, balance amount + // Insert row with name, address, and balance amount. fmt.Fprintf(tabw, "%s\t%s\t%s\n", key.GetName(), address.String(), qret.BaseAccount.GetCoins().String()) } - // Flush table + // Flush table. tabw.Flush() headline := fmt.Sprintf("(%d) known accounts", len(keys)) @@ -192,17 +131,22 @@ func logAccounts(logger *slog.Logger, kb keys.Keybase, _ *dev.Node) error { return nil } -// createAccount creates a new account with the given name and adds it to the keybase. -func createAccount(kb keys.Keybase, accountName string) (keys.Info, error) { +// CreateAccount creates a new account with the given name and adds it to the keybase. +func createAccount(kb keys.Keybase, accountName string) (keys.Info, string, error) { entropy, err := bip39.NewEntropy(256) if err != nil { - return nil, fmt.Errorf("error creating entropy: %w", err) + return nil, "", fmt.Errorf("error creating entropy: %w", err) } mnemonic, err := bip39.NewMnemonic(entropy) if err != nil { - return nil, fmt.Errorf("error generating mnemonic: %w", err) + return nil, "", fmt.Errorf("error generating mnemonic: %w", err) + } + + key, err := kb.CreateAccount(accountName, mnemonic, "", "", 0, 0) + if err != nil { + return nil, "", err } - return kb.CreateAccount(accountName, mnemonic, "", "", 0, 0) + return key, mnemonic, nil } diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 945f92c9ce6..958e2027ed2 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -27,6 +27,7 @@ const ( WebLogName = "GnoWeb" KeyPressLogName = "KeyPress" EventServerLogName = "Event" + AccountsLogName = "Accounts" ) var ( @@ -43,11 +44,11 @@ type devCfg struct { nodeProxyAppListenerAddr string // Users default - genesisCreator string - home string - root string - additionalUsers varAccounts - balancesFile string + genesisCreator string + home string + root string + additionalAccounts varAccounts + balancesFile string // Node Configuration minimal bool @@ -125,9 +126,9 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { ) fs.Var( - &c.additionalUsers, - "add-user", - "pre-add a user", + &c.additionalAccounts, + "add-account", + "add and set account(s) in the form `[:]`, can be use multiple time", ) fs.StringVar( @@ -209,7 +210,7 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { emitterServer := emitter.NewServer(loggerEvents) // load keybase - kb, err := setupKeybase(logger, cfg) + kb, err := setupKeybase(logger.WithGroup(AccountsLogName), cfg) if err != nil { return fmt.Errorf("unable to load keybase: %w", err) } @@ -279,7 +280,7 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { } var helper string = ` -A Accounts - Display known accounts +A Accounts - Display known accounts and balances H Help - Display this message R Reload - Reload all packages to take change into account. Ctrl+R Reset - Reset application state. @@ -330,7 +331,7 @@ func runEventLoop( case rawterm.KeyH: // Helper logger.Info("Gno Dev Helper", "helper", helper) case rawterm.KeyA: // Accounts - logAccounts(logger.WithGroup("accounts"), kb, dnode) + logAccounts(logger.WithGroup(AccountsLogName), kb, dnode) case rawterm.KeyR: // Reload logger.WithGroup(NodeLogName).Info("reloading...") if err = dnode.ReloadAll(ctx); err != nil { diff --git a/contribs/gnodev/cmd/gnodev/setup_keybase.go b/contribs/gnodev/cmd/gnodev/setup_keybase.go new file mode 100644 index 00000000000..242affb935d --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/setup_keybase.go @@ -0,0 +1,109 @@ +package main + +import ( + "fmt" + "log/slog" + + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/crypto/keys/keyerror" + osm "github.com/gnolang/gno/tm2/pkg/os" +) + +func setupKeybase(logger *slog.Logger, cfg *devCfg) (keys.Keybase, error) { + kb := keys.NewInMemory() + + // Check for home folder + if cfg.home == "" { + logger.Warn("local keybase disabled") + } else if !osm.DirExists(cfg.home) { + logger.Warn("keybase directory does not exist, no local keys will be imported", + "path", cfg.home) + } else if err := importKeybase(kb, cfg.home); err != nil { + return nil, fmt.Errorf("unable to import local keybase %q: %w", cfg.home, err) + } + + // Add additional accounts to our keybase + for acc := range cfg.additionalAccounts { + // Check if the account exist in the local keybase + if ok, _ := kb.HasByName(acc); ok { + continue + } + + // Check if we have a valid bech32 address instead + addr, err := crypto.AddressFromBech32(acc) + if err != nil { + return nil, fmt.Errorf("invalid bech32 address or unkown key %q", acc) + } + + // If we already know this address from keybase, skip it + ok, err := kb.HasByAddress(addr) + if ok { + continue + } + + // We don't know this address, then add it to our keybase + pub, err := crypto.PubKeyFromBech32(acc) + if err != nil { + return nil, fmt.Errorf("unable to get PubKey from %q: %w", acc, err) + } + + name := fmt.Sprintf("_account#%.6s", addr.String()) + info, err := kb.CreateOffline(name, pub) + if err != nil { + return nil, fmt.Errorf("unable to add additional account: %w", err) + } + + logger.Info("additional account added", + "name", info.GetName(), + "addr", info.GetAddress()) + } + + // Ensure that we have a default address + info, err := kb.GetByAddress(DefaultCreatorAddress) + switch { + case err == nil: // Account already exist in the keybase + logger.Info("default address imported from keybase", "name", info.GetName(), "addr", info.GetAddress()) + case keyerror.IsErrKeyNotFound(err): + // If the key isn't found, create a default one + creatorName := fmt.Sprintf("_default#%.6s", DefaultCreatorAddress.String()) + if ok, _ := kb.HasByName(creatorName); ok { + return nil, fmt.Errorf("unable to create default account, %q already exist in imported keybase", creatorName) + } + + info, err = kb.CreateAccount(creatorName, DefaultCreatorSeed, "", "", 0, 0) + if err != nil { + return nil, fmt.Errorf("unable to create default account %q: %w", DefaultCreatorName, err) + } + + logger.Warn("default address created", + "name", info.GetName(), + "addr", info.GetAddress(), + "mnemonic", DefaultCreatorSeed, + ) + default: + return nil, fmt.Errorf("unable to get address %q: %w", info.GetAddress(), err) + } + + return kb, nil +} + +func importKeybase(to keys.Keybase, path string) error { + // Load home keybase into our inMemory keybase + from, err := keys.NewKeyBaseFromDir(path) + if err != nil { + return fmt.Errorf("unable to load keybase: %w", err) + } + + keys, err := from.List() + if err != nil { + return fmt.Errorf("unable to list keys: %w", err) + } + + for _, key := range keys { + name := key.GetName() + to.CreateOffline(name, key.GetPubKey()) + } + + return nil +} diff --git a/docs/gno-tooling/cli/gnodev.md b/docs/gno-tooling/cli/gnodev.md index a0b739e6df6..37e4faf1332 100644 --- a/docs/gno-tooling/cli/gnodev.md +++ b/docs/gno-tooling/cli/gnodev.md @@ -15,8 +15,7 @@ local instance of `gnoweb`, allowing you to see the rendering of your Gno code i the **examples** folder and any user-specified paths. - **Web Interface Server**: Gnodev automatically starts a `gnoweb` server on [`localhost:8888`](https://localhost:8888). -- **Balances and Keybase Customization**: Users can specify balances, load from - a balances file or create temporary users. +- **Balances and Keybase Customization**: Users can set balances, load from a file, or add users via a flag. - **Hot Reload**: Gnodev monitors the **examples** folder and any specified for file changes, reloading and automatically restarting the node as needed. - **State Maintenance**: Gnodev replays all transactions in between reloads, @@ -47,16 +46,16 @@ realm with specific creator and deposit: gnodev ./myrealm?creator=foo&deposit=42ugnot`` ``` -### Additional User -Use `-add-user` flag in the format (:) to add temporary users. You can repeat this to add -multiple users. Addresses of those will be displayed at runtime, or by pressing `A` interactively to display -accounts. +### Additional Account +Use the `-add-account` flag with the format `[:]` to add a specific address or key name +from your local keybase. You can set an optional amount for this address. Repeat this command to add multiple +accounts. The addresses will be shown during runtime or by pressing `A` to display accounts interactively. ## Interactive Usage While `gnodev` is running, the following shortcuts are available: - To see help, press `H`. -- To display accounts, press `A`. +- To display accounts balances, press `A`. - To reload manually, press `R`. - To reset the state of the node, press `CMD+R`. - To stop `gnodev`, press `CMD+C`. @@ -67,7 +66,7 @@ While `gnodev` is running, the following shortcuts are available: |---------------------|---------------------------------------------------------| | --minimal | Start `gnodev` without loading the examples folder. | | --no-watch | Disable hot reload. | -| --add-user | Pre-add user(s) in the form (:) | +| --add-account | Pre-add account(s) in the form `[:]` | | --balances-file | Load a balance for the user(s) from a balance file. | | --chain-id | Set node ChainID | | --genesis-creator | Name or bech32 address of the genesis creator | From ecc305f43be80c2ed7f1a5bdbb2f808d6a62090d Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 18 Apr 2024 09:45:21 +0200 Subject: [PATCH 08/23] fix: lint Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/gnoland/types.go | 30 +++++++++++++++--------------- gno.land/pkg/gnoland/types_test.go | 4 ++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index d0ac52b40b0..36ea3384c3c 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -86,31 +86,31 @@ func NewBalances() Balances { return make(Balances) } -func (balances Balances) Set(address crypto.Address, amount std.Coins) { - balances[address] = Balance{ +func (bs Balances) Set(address crypto.Address, amount std.Coins) { + bs[address] = Balance{ Address: address, Amount: amount, } } -func (balances Balances) Get(address crypto.Address) (balance Balance, ok bool) { - balance, ok = balances[address] +func (bs Balances) Get(address crypto.Address) (balance Balance, ok bool) { + balance, ok = bs[address] return } -func (balances Balances) List() []Balance { - list := make([]Balance, 0, len(balances)) - for _, balance := range balances { +func (bs Balances) List() []Balance { + list := make([]Balance, 0, len(bs)) + for _, balance := range bs { list = append(list, balance) } return list } // leftMerge left-merges the two maps -func (a Balances) LeftMerge(b Balances) { - for key, bVal := range b { - if _, present := (a)[key]; !present { - (a)[key] = bVal +func (bs Balances) LeftMerge(from Balances) { + for key, bVal := range from { + if _, present := (bs)[key]; !present { + (bs)[key] = bVal } } } @@ -121,13 +121,13 @@ func GetBalancesFromEntries(entries ...string) (Balances, error) { } // LoadFromEntries extracts the balance entries in the form of
= -func (balances Balances) LoadFromEntries(entries ...string) error { +func (bs Balances) LoadFromEntries(entries ...string) error { for _, entry := range entries { var balance Balance if err := balance.Parse(entry); err != nil { return fmt.Errorf("unable to parse balance entry: %w", err) } - balances[balance.Address] = balance + bs[balance.Address] = balance } return nil @@ -140,7 +140,7 @@ func GetBalancesFromSheet(sheet io.Reader) (Balances, error) { // LoadFromSheet extracts the balance sheet from the passed in // balance sheet file, that has the format of
=ugnot -func (balances Balances) LoadFromSheet(sheet io.Reader) error { +func (bs Balances) LoadFromSheet(sheet io.Reader) error { // Parse the balances scanner := bufio.NewScanner(sheet) @@ -156,7 +156,7 @@ func (balances Balances) LoadFromSheet(sheet io.Reader) error { continue } - if err := balances.LoadFromEntries(entry); err != nil { + if err := bs.LoadFromEntries(entry); err != nil { return fmt.Errorf("unable to load entries: %w", err) } } diff --git a/gno.land/pkg/gnoland/types_test.go b/gno.land/pkg/gnoland/types_test.go index 00e5d46d996..0cc2424fc7e 100644 --- a/gno.land/pkg/gnoland/types_test.go +++ b/gno.land/pkg/gnoland/types_test.go @@ -178,7 +178,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { balanceMap, err := GetBalancesFromEntries(balances...) assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, "invalid amount") + assert.Contains(t, err.Error(), "invalid amount") }) } @@ -231,7 +231,7 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) { balanceMap, err := GetBalancesFromSheet(reader) assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, "invalid amount") + assert.Contains(t, err.Error(), "invalid amount") }) } From 690be026e2f8ca2acb0ad75722f83c7b9d6eff84 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:27:53 +0200 Subject: [PATCH 09/23] chore: update genesis-creator to deploy-key Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/main.go | 20 ++++----- docs/gno-tooling/cli/gnodev.md | 72 ++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 34 deletions(-) diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 958e2027ed2..aafbba6ab1d 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -44,7 +44,7 @@ type devCfg struct { nodeProxyAppListenerAddr string // Users default - genesisCreator string + deployKey string home string root string additionalAccounts varAccounts @@ -65,7 +65,7 @@ var defaultDevOptions = &devCfg{ maxGas: 10_000_000_000, webListenerAddr: "127.0.0.1:8888", nodeRPCListenerAddr: "127.0.0.1:36657", - genesisCreator: DefaultCreatorAddress.String(), + deployKey: DefaultCreatorAddress.String(), home: gnoenv.HomeDir(), root: gnoenv.RootDir(), @@ -132,10 +132,10 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { ) fs.StringVar( - &c.genesisCreator, - "genesis-creator", - defaultDevOptions.genesisCreator, - "name or bech32 address of the genesis creator", + &c.deployKey, + "deploy-key", + defaultDevOptions.deployKey, + "default key name or Bech32 address for deploying packages", ) fs.BoolVar( @@ -376,13 +376,13 @@ func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm. func resolvePackagesPathFromArgs(cfg *devCfg, kb keys.Keybase, args []string) ([]gnodev.PackagePath, error) { paths := make([]gnodev.PackagePath, 0, len(args)) - if cfg.genesisCreator == "" { - return nil, fmt.Errorf("default genesis creator cannot be empty") + if cfg.deployKey == "" { + return nil, fmt.Errorf("default deploy key cannot be empty") } - defaultKey, err := kb.GetByNameOrAddress(cfg.genesisCreator) + defaultKey, err := kb.GetByNameOrAddress(cfg.deployKey) if err != nil { - return nil, fmt.Errorf("unable to get genesis creator %q: %w", cfg.genesisCreator, err) + return nil, fmt.Errorf("unable to get deploy key %q: %w", cfg.deployKey, err) } for _, arg := range args { diff --git a/docs/gno-tooling/cli/gnodev.md b/docs/gno-tooling/cli/gnodev.md index 37e4faf1332..b255ceaab84 100644 --- a/docs/gno-tooling/cli/gnodev.md +++ b/docs/gno-tooling/cli/gnodev.md @@ -22,6 +22,7 @@ local instance of `gnoweb`, allowing you to see the rendering of your Gno code i ensuring the previous node state is preserved. ## Installation + Gnodev can be found in the `contribs` folder in the monorepo. To install `gnodev`, run `make install`. @@ -38,18 +39,41 @@ gnodev ./myrealm ## Keybase and Balance -Gnodev will, by default, load your keybase located in the GNOHOME directory, giving all your keys (almost) unlimited fund. +Gnodev will, by default, load the keybase located in your GNOHOME directory, pre-mining `10e12` amount of +ugnot to all of them. This way, users can interact with Gnodev's in-memory node out of the box. The addresses +and their respective balance can be shown at runtime by pressing `A` to display accounts interactively. + +### Additional Account + +To add or set a specific address or key name from your local Keybase with an optional amount, use the +`--add-account` flag in the format `[:]`. You can use this command multiple times to add +or set multiple accounts: -All realms will be by the `-genesis-creator`, but You can also pass query options to the realm path to load a -realm with specific creator and deposit: ``` -gnodev ./myrealm?creator=foo&deposit=42ugnot`` +gnodev --add-acount=g1...:42ugnot --add-acount=test2:42ugnot ``` -### Additional Account -Use the `-add-account` flag with the format `[:]` to add a specific address or key name -from your local keybase. You can set an optional amount for this address. Repeat this command to add multiple -accounts. The addresses will be shown during runtime or by pressing `A` to display accounts interactively. +### Deploy + +All realms and packages will be deployed to the in-memory node by the address passed in with the +`--deploy-key` flag. The `deploy-key` address can be changed for a specific package or realm by passing in +the desired address (or a known keyname) using with the following pattern: + +``` +gnodev ./myrealm?deployer=g1.... +``` + +A specific deposit amount can also be set with the following pattern: + +``` +gnodev ./myrealm?deposit=42ugnot +``` + +This patten can be expanded to accommodate both options: + +``` +gnodev ./myrealm?deployer=&deposit= +``` ## Interactive Usage @@ -62,19 +86,19 @@ While `gnodev` is running, the following shortcuts are available: ### Options -| Flag | Effect | -|---------------------|---------------------------------------------------------| -| --minimal | Start `gnodev` without loading the examples folder. | -| --no-watch | Disable hot reload. | -| --add-account | Pre-add account(s) in the form `[:]` | -| --balances-file | Load a balance for the user(s) from a balance file. | -| --chain-id | Set node ChainID | -| --genesis-creator | Name or bech32 address of the genesis creator | -| --home | Set the path to load user's Keybase. | -| --max-gas | Set the maximum gas per block | -| --no-replay | Do not replay previous transactions upon reload | -| --node-rpc-listener | listening address for GnoLand RPC node | -| --root | gno root directory | -| --server-mode | disable interaction, and adjust logging for server use. | -| --verbose | enable verbose output for development | -| --web-listener | web server listening address | +| Flag | Effect | +|---------------------|------------------------------------------------------------| +| --minimal | Start `gnodev` without loading the examples folder. | +| --no-watch | Disable hot reload. | +| --add-account | Pre-add account(s) in the form `[:]` | +| --balances-file | Load a balance for the user(s) from a balance file. | +| --chain-id | Set node ChainID | +| --deploy-key | Default key name or Bech32 address for uploading packages. | +| --home | Set the path to load user's Keybase. | +| --max-gas | Set the maximum gas per block | +| --no-replay | Do not replay previous transactions upon reload | +| --node-rpc-listener | listening address for GnoLand RPC node | +| --root | gno root directory | +| --server-mode | disable interaction, and adjust logging for server use. | +| --verbose | enable verbose output for development | +| --web-listener | web server listening address | From fb98ef5a6ff3650e9b41033c759e39e360401147 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 18 Apr 2024 17:51:50 +0200 Subject: [PATCH 10/23] chore: update creator to deployer Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/main.go | 8 ++++---- contribs/gnodev/cmd/gnodev/setup_keybase.go | 10 +++++----- contribs/gnodev/pkg/dev/node.go | 14 +++++++------- contribs/gnodev/pkg/dev/packages.go | 2 +- contribs/gnodev/pkg/dev/packages_test.go | 10 +++++----- docs/gno-tooling/cli/gnodev.md | 21 +++++++++++++-------- 6 files changed, 35 insertions(+), 30 deletions(-) diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index aafbba6ab1d..edeb92980b3 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -31,9 +31,9 @@ const ( ) var ( - DefaultCreatorName = integration.DefaultAccount_Name - DefaultCreatorAddress = crypto.MustAddressFromString(integration.DefaultAccount_Address) - DefaultCreatorSeed = integration.DefaultAccount_Seed + DefaultDeployerName = integration.DefaultAccount_Name + DefaultDeployerAddress = crypto.MustAddressFromString(integration.DefaultAccount_Address) + DefaultDeployerSeed = integration.DefaultAccount_Seed ) type devCfg struct { @@ -65,7 +65,7 @@ var defaultDevOptions = &devCfg{ maxGas: 10_000_000_000, webListenerAddr: "127.0.0.1:8888", nodeRPCListenerAddr: "127.0.0.1:36657", - deployKey: DefaultCreatorAddress.String(), + deployKey: DefaultDeployerAddress.String(), home: gnoenv.HomeDir(), root: gnoenv.RootDir(), diff --git a/contribs/gnodev/cmd/gnodev/setup_keybase.go b/contribs/gnodev/cmd/gnodev/setup_keybase.go index 242affb935d..bea96293f41 100644 --- a/contribs/gnodev/cmd/gnodev/setup_keybase.go +++ b/contribs/gnodev/cmd/gnodev/setup_keybase.go @@ -60,26 +60,26 @@ func setupKeybase(logger *slog.Logger, cfg *devCfg) (keys.Keybase, error) { } // Ensure that we have a default address - info, err := kb.GetByAddress(DefaultCreatorAddress) + info, err := kb.GetByAddress(DefaultDeployerAddress) switch { case err == nil: // Account already exist in the keybase logger.Info("default address imported from keybase", "name", info.GetName(), "addr", info.GetAddress()) case keyerror.IsErrKeyNotFound(err): // If the key isn't found, create a default one - creatorName := fmt.Sprintf("_default#%.6s", DefaultCreatorAddress.String()) + creatorName := fmt.Sprintf("_default#%.6s", DefaultDeployerAddress.String()) if ok, _ := kb.HasByName(creatorName); ok { return nil, fmt.Errorf("unable to create default account, %q already exist in imported keybase", creatorName) } - info, err = kb.CreateAccount(creatorName, DefaultCreatorSeed, "", "", 0, 0) + info, err = kb.CreateAccount(creatorName, DefaultDeployerSeed, "", "", 0, 0) if err != nil { - return nil, fmt.Errorf("unable to create default account %q: %w", DefaultCreatorName, err) + return nil, fmt.Errorf("unable to create default account %q: %w", DefaultDeployerName, err) } logger.Warn("default address created", "name", info.GetName(), "addr", info.GetAddress(), - "mnemonic", DefaultCreatorSeed, + "mnemonic", DefaultDeployerSeed, ) default: return nil, fmt.Errorf("unable to get address %q: %w", info.GetAddress(), err) diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 971d3010bb1..66971980b73 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -28,7 +28,7 @@ import ( ) type NodeConfig struct { - DefaultCreator crypto.Address + DefaultDeployer crypto.Address BalancesList []gnoland.Balance PackagesPathList []PackagePath TMConfig *tmcfg.Config @@ -42,16 +42,16 @@ func DefaultNodeConfig(rootdir string) *NodeConfig { tmc := gnoland.NewDefaultTMConfig(rootdir) tmc.Consensus.SkipTimeoutCommit = false // avoid time drifting, see issue #1507 - defaultCreator := crypto.MustAddressFromString(integration.DefaultAccount_Address) + defaultDeployer := crypto.MustAddressFromString(integration.DefaultAccount_Address) balances := []gnoland.Balance{ { - Address: defaultCreator, + Address: defaultDeployer, Amount: std.Coins{std.NewCoin("ugnot", 10e12)}, }, } return &NodeConfig{ - DefaultCreator: defaultCreator, + DefaultDeployer: defaultDeployer, BalancesList: balances, ChainID: tmc.ChainID(), PackagesPathList: []PackagePath{}, @@ -142,14 +142,14 @@ func (n *Node) UpdatePackages(paths ...string) error { return fmt.Errorf("unable to resolve abs path of %q: %w", path, err) } - creator := n.config.DefaultCreator + deployer := n.config.DefaultDeployer var deposit std.Coins for _, ppath := range n.config.PackagesPathList { if !strings.HasPrefix(abspath, ppath.Path) { continue } - creator = ppath.Creator + deployer = ppath.Creator deposit = ppath.Deposit } @@ -163,7 +163,7 @@ func (n *Node) UpdatePackages(paths ...string) error { for _, pkg := range pkgslist { n.pkgs[pkg.Dir] = Package{ Pkg: pkg, - Creator: creator, + Creator: deployer, Deposit: deposit, } diff --git a/contribs/gnodev/pkg/dev/packages.go b/contribs/gnodev/pkg/dev/packages.go index bf69338b2b5..0dd67e61ac4 100644 --- a/contribs/gnodev/pkg/dev/packages.go +++ b/contribs/gnodev/pkg/dev/packages.go @@ -132,7 +132,7 @@ func (pm PackagesMap) Load(fee std.Fee) ([]std.Tx, error) { for _, modPkg := range nonDraft { pkg := pm[modPkg.Dir] if pkg.Creator.IsZero() { - return nil, fmt.Errorf("no creator was set for %q", pkg.Dir) + return nil, fmt.Errorf("no creator set for %q", pkg.Dir) } // Open files in directory as MemPackage. diff --git a/contribs/gnodev/pkg/dev/packages_test.go b/contribs/gnodev/pkg/dev/packages_test.go index e0941e29d98..b262617c09f 100644 --- a/contribs/gnodev/pkg/dev/packages_test.go +++ b/contribs/gnodev/pkg/dev/packages_test.go @@ -34,23 +34,23 @@ func TestResolvePackagePathQuery(t *testing.T) { {"/ambiguo/u//s/path///", PackagePath{ Path: "/ambiguo/u/s/path", }, false}, - {"/path/with/creator?creator=testAccount", PackagePath{ - Path: "/path/with/creator", + {"/path/with/deployer?deployer=testAccount", PackagePath{ + Path: "/path/with/deployer", Creator: testingAddress, }, false}, {"/path/with/deposit?deposit=100ugnot", PackagePath{ Path: "/path/with/deposit", Deposit: std.MustParseCoins("100ugnot"), }, false}, - {".?creator=g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na&deposit=100ugnot", PackagePath{ + {".?deployer=g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na&deposit=100ugnot", PackagePath{ Path: ".", Creator: testingAddress, Deposit: std.MustParseCoins("100ugnot"), }, false}, // errors cases - {"/invalid/account?creator=UnknownAccount", PackagePath{}, true}, - {"/invalid/address?creator=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", PackagePath{}, true}, + {"/invalid/account?deployer=UnknownAccount", PackagePath{}, true}, + {"/invalid/address?deployer=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", PackagePath{}, true}, {"/invalid/deposit?deposit=abcd", PackagePath{}, true}, } diff --git a/docs/gno-tooling/cli/gnodev.md b/docs/gno-tooling/cli/gnodev.md index b255ceaab84..2f0b340d629 100644 --- a/docs/gno-tooling/cli/gnodev.md +++ b/docs/gno-tooling/cli/gnodev.md @@ -43,24 +43,29 @@ Gnodev will, by default, load the keybase located in your GNOHOME directory, pre ugnot to all of them. This way, users can interact with Gnodev's in-memory node out of the box. The addresses and their respective balance can be shown at runtime by pressing `A` to display accounts interactively. -### Additional Account +### Adding or Updating Accounts -To add or set a specific address or key name from your local Keybase with an optional amount, use the -`--add-account` flag in the format `[:]`. You can use this command multiple times to add -or set multiple accounts: +Utilize the `--add-account` flag to add a new account or update an existing one in your local Keybase, +following the format `[:]`. The `` represents the specific key name or +address, and `` is an optional limitation on the account. + +Example of use: ``` -gnodev --add-acount=g1...:42ugnot --add-acount=test2:42ugnot +gnodev --add-account [:] --add-account [:] ... ``` +Please note: If the address exists in your local Keybase, the `--add-account` flag will only update its amount, +instead of creating a duplicate. + ### Deploy All realms and packages will be deployed to the in-memory node by the address passed in with the `--deploy-key` flag. The `deploy-key` address can be changed for a specific package or realm by passing in -the desired address (or a known keyname) using with the following pattern: +the desired address (or a known key name) using with the following pattern: ``` -gnodev ./myrealm?deployer=g1.... +gnodev ./myrealm?creator=g1.... ``` A specific deposit amount can also be set with the following pattern: @@ -72,7 +77,7 @@ gnodev ./myrealm?deposit=42ugnot This patten can be expanded to accommodate both options: ``` -gnodev ./myrealm?deployer=&deposit= +gnodev ./myrealm?creator=&deposit= ``` ## Interactive Usage From 1012bec9122a58fc74ec708fc0f4d732ca4c2b61 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 18 Apr 2024 18:01:23 +0200 Subject: [PATCH 11/23] chore: lint Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- docs/gno-tooling/cli/gnodev.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/gno-tooling/cli/gnodev.md b/docs/gno-tooling/cli/gnodev.md index 2f0b340d629..a0e0fb7aeee 100644 --- a/docs/gno-tooling/cli/gnodev.md +++ b/docs/gno-tooling/cli/gnodev.md @@ -15,8 +15,9 @@ local instance of `gnoweb`, allowing you to see the rendering of your Gno code i the **examples** folder and any user-specified paths. - **Web Interface Server**: Gnodev automatically starts a `gnoweb` server on [`localhost:8888`](https://localhost:8888). -- **Balances and Keybase Customization**: Users can set balances, load from a file, or add users via a flag. -- **Hot Reload**: Gnodev monitors the **examples** folder and any specified for +- **Balances and Keybase Customization**: Users can set account balances, load them from a file, or add new + accounts via a flag. +- **Hot Reload**: Gnodev monitors the **examples** folder, as well as any folder specified as an argument for file changes, reloading and automatically restarting the node as needed. - **State Maintenance**: Gnodev replays all transactions in between reloads, ensuring the previous node state is preserved. From b2ae172dd98723398ce5e4848e479a30e387da83 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:45:28 +0200 Subject: [PATCH 12/23] chore: fix typo Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index edeb92980b3..3cecccc3f7e 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -128,7 +128,7 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { fs.Var( &c.additionalAccounts, "add-account", - "add and set account(s) in the form `[:]`, can be use multiple time", + "add and set account(s) in the form `[:]`, can be used multiple time", ) fs.StringVar( From 9cfd3d73e0cdb86fb61215e1aa8a566aa2fac53a Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 23 Apr 2024 09:52:25 +0200 Subject: [PATCH 13/23] fix: add balance file flag Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/main.go | 7 +++++++ docs/gno-tooling/cli/gnodev.md | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 3cecccc3f7e..5c279871ac5 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -131,6 +131,13 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { "add and set account(s) in the form `[:]`, can be used multiple time", ) + fs.StringVar( + &c.balancesFile, + "balance-file", + defaultDevOptions.balancesFile, + "load the provided balance file (refer to the documentation for format)", + ) + fs.StringVar( &c.deployKey, "deploy-key", diff --git a/docs/gno-tooling/cli/gnodev.md b/docs/gno-tooling/cli/gnodev.md index a0e0fb7aeee..74d60739f0c 100644 --- a/docs/gno-tooling/cli/gnodev.md +++ b/docs/gno-tooling/cli/gnodev.md @@ -59,6 +59,19 @@ gnodev --add-account [:] --add-account [: Date: Tue, 23 Apr 2024 09:52:45 +0200 Subject: [PATCH 14/23] chore: rename varAccounts into varPremineAccounts Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/accounts.go | 27 +++----------------------- contribs/gnodev/cmd/gnodev/main.go | 2 +- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/contribs/gnodev/cmd/gnodev/accounts.go b/contribs/gnodev/cmd/gnodev/accounts.go index 0c40bf610cc..3f907a74a48 100644 --- a/contribs/gnodev/cmd/gnodev/accounts.go +++ b/contribs/gnodev/cmd/gnodev/accounts.go @@ -11,14 +11,13 @@ import ( "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - "github.com/gnolang/gno/tm2/pkg/crypto/bip39" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/std" ) -type varAccounts map[string]std.Coins // name or bech32 to coins. +type varPremineAccounts map[string]std.Coins // name or bech32 to coins. -func (va *varAccounts) Set(value string) error { +func (va *varPremineAccounts) Set(value string) error { if *va == nil { *va = map[string]std.Coins{} } @@ -40,7 +39,7 @@ func (va *varAccounts) Set(value string) error { return nil } -func (va varAccounts) String() string { +func (va varPremineAccounts) String() string { accs := make([]string, 0, len(va)) for user, balance := range va { accs = append(accs, fmt.Sprintf("%s(%s)", user, balance.String())) @@ -130,23 +129,3 @@ func logAccounts(logger *slog.Logger, kb keys.Keybase, _ *dev.Node) error { logger.Info(headline, "table", tab.String()) return nil } - -// CreateAccount creates a new account with the given name and adds it to the keybase. -func createAccount(kb keys.Keybase, accountName string) (keys.Info, string, error) { - entropy, err := bip39.NewEntropy(256) - if err != nil { - return nil, "", fmt.Errorf("error creating entropy: %w", err) - } - - mnemonic, err := bip39.NewMnemonic(entropy) - if err != nil { - return nil, "", fmt.Errorf("error generating mnemonic: %w", err) - } - - key, err := kb.CreateAccount(accountName, mnemonic, "", "", 0, 0) - if err != nil { - return nil, "", err - } - - return key, mnemonic, nil -} diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 5c279871ac5..9be4c13cf25 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -47,7 +47,7 @@ type devCfg struct { deployKey string home string root string - additionalAccounts varAccounts + additionalAccounts varPremineAccounts balancesFile string // Node Configuration From 87ec0b316db2114c855ad4e87baa47552ab00ecd Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:34:11 +0200 Subject: [PATCH 15/23] feat: add addressBook in gnodev to replace keybase Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/accounts.go | 80 +++++++---- contribs/gnodev/cmd/gnodev/main.go | 49 ++++--- .../gnodev/cmd/gnodev/setup_address_book.go | 65 +++++++++ contribs/gnodev/cmd/gnodev/setup_keybase.go | 109 -------------- contribs/gnodev/cmd/gnodev/setup_node.go | 9 +- contribs/gnodev/pkg/address/book.go | 133 ++++++++++++++++++ contribs/gnodev/pkg/address/book_test.go | 109 ++++++++++++++ contribs/gnodev/pkg/dev/packages.go | 11 +- 8 files changed, 390 insertions(+), 175 deletions(-) create mode 100644 contribs/gnodev/cmd/gnodev/setup_address_book.go delete mode 100644 contribs/gnodev/cmd/gnodev/setup_keybase.go create mode 100644 contribs/gnodev/pkg/address/book.go create mode 100644 contribs/gnodev/pkg/address/book_test.go diff --git a/contribs/gnodev/cmd/gnodev/accounts.go b/contribs/gnodev/cmd/gnodev/accounts.go index 3f907a74a48..e725aa1017d 100644 --- a/contribs/gnodev/cmd/gnodev/accounts.go +++ b/contribs/gnodev/cmd/gnodev/accounts.go @@ -7,11 +7,11 @@ import ( "strings" "text/tabwriter" + "github.com/gnolang/gno/contribs/gnodev/pkg/address" "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -23,7 +23,7 @@ func (va *varPremineAccounts) Set(value string) error { } accounts := *va - user, amount, found := strings.Cut(value, ":") + user, amount, found := strings.Cut(value, "=") accounts[user] = nil if !found { return nil @@ -48,25 +48,32 @@ func (va varPremineAccounts) String() string { return strings.Join(accs, ",") } -func generateBalances(kb keys.Keybase, cfg *devCfg) (gnoland.Balances, error) { +func generateBalances(bk *address.Book, cfg *devCfg) (gnoland.Balances, error) { bls := gnoland.NewBalances() unlimitedFund := std.Coins{std.NewCoin("ugnot", 10e12)} - keys, err := kb.List() - if err != nil { - return nil, fmt.Errorf("unable to list keys from keybase: %w", err) - } + entries := bk.List() // Automatically set every key from keybase to unlimited fund. - for _, key := range keys { - address := key.GetAddress() + for _, entry := range entries { + address := entry.Address // Check if a predefined amount has been set for this key. + + // Check for address + if preDefinedFound, ok := cfg.premineAccounts[address.String()]; ok && preDefinedFound != nil { + bls[address] = gnoland.Balance{Amount: preDefinedFound, Address: address} + continue + } + + // Check for name found := unlimitedFund - if preDefinedFound, ok := cfg.additionalAccounts[key.GetName()]; ok && preDefinedFound != nil { - found = preDefinedFound - } else if preDefinedFound, ok := cfg.additionalAccounts[address.String()]; ok && preDefinedFound != nil { - found = preDefinedFound + for _, name := range entry.Names { + if preDefinedFound, ok := cfg.premineAccounts[name]; ok && preDefinedFound != nil { + found = preDefinedFound + break + } + } bls[address] = gnoland.Balance{Amount: found, Address: address} @@ -76,6 +83,8 @@ func generateBalances(kb keys.Keybase, cfg *devCfg) (gnoland.Balances, error) { return bls, nil } + // Load balance file + file, err := os.Open(cfg.balancesFile) if err != nil { return nil, fmt.Errorf("unable to open balance file %q: %w", cfg.balancesFile, err) @@ -86,30 +95,30 @@ func generateBalances(kb keys.Keybase, cfg *devCfg) (gnoland.Balances, error) { return nil, fmt.Errorf("unable to read balances file %q: %w", cfg.balancesFile, err) } + // Add balance address to AddressBook + for addr := range blsFile { + bk.Add(addr, "") + } + // Left merge keybase balance into loaded file balance. + // TL;DR: balance file override every balance at the end blsFile.LeftMerge(bls) return blsFile, nil } -func logAccounts(logger *slog.Logger, kb keys.Keybase, _ *dev.Node) error { - keys, err := kb.List() - if err != nil { - return fmt.Errorf("unable to get keybase keys list: %w", err) - } - +func logAccounts(logger *slog.Logger, book *address.Book, _ *dev.Node) error { var tab strings.Builder tabw := tabwriter.NewWriter(&tab, 0, 0, 2, ' ', tabwriter.TabIndent) + entries := book.List() + fmt.Fprintln(tabw, "KeyName\tAddress\tBalance") // Table header. - for _, key := range keys { - if key.GetName() == "" { - continue // Skip empty key name. - } - address := key.GetAddress() - qres, err := client.NewLocal().ABCIQuery("auth/accounts/"+address.String(), []byte{}) + for _, entry := range entries { + address := entry.Address.String() + qres, err := client.NewLocal().ABCIQuery("auth/accounts/"+address, []byte{}) if err != nil { - return fmt.Errorf("unable to query account %q: %w", address.String(), err) + return fmt.Errorf("unable to query account %q: %w", address, err) } var qret struct{ BaseAccount std.BaseAccount } @@ -117,15 +126,24 @@ func logAccounts(logger *slog.Logger, kb keys.Keybase, _ *dev.Node) error { return fmt.Errorf("unable to unmarshal query response: %w", err) } - // Insert row with name, address, and balance amount. - fmt.Fprintf(tabw, "%s\t%s\t%s\n", key.GetName(), - address.String(), - qret.BaseAccount.GetCoins().String()) + if len(entry.Names) == 0 { + // Insert row with name, address, and balance amount. + fmt.Fprintf(tabw, "%s\t%s\t%s\n", "_", address, qret.BaseAccount.GetCoins().String()) + continue + } + + for _, name := range entry.Names { + // Insert row with name, address, and balance amount. + fmt.Fprintf(tabw, "%s\t%s\t%s\n", name, + address, + qret.BaseAccount.GetCoins().String()) + } } + // Flush table. tabw.Flush() - headline := fmt.Sprintf("(%d) known accounts", len(keys)) + headline := fmt.Sprintf("(%d) known keys", len(entries)) logger.Info(headline, "table", tab.String()) return nil } diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 9be4c13cf25..0f3e962feaf 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -10,6 +10,7 @@ import ( "path/filepath" "time" + "github.com/gnolang/gno/contribs/gnodev/pkg/address" gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" @@ -18,7 +19,6 @@ import ( "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/crypto/keys" osm "github.com/gnolang/gno/tm2/pkg/os" ) @@ -44,11 +44,11 @@ type devCfg struct { nodeProxyAppListenerAddr string // Users default - deployKey string - home string - root string - additionalAccounts varPremineAccounts - balancesFile string + deployKey string + home string + root string + premineAccounts varPremineAccounts + balancesFile string // Node Configuration minimal bool @@ -126,9 +126,9 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { ) fs.Var( - &c.additionalAccounts, + &c.premineAccounts, "add-account", - "add and set account(s) in the form `[:]`, can be used multiple time", + "add (or set) a premine account in the form `[:]`, can be used multiple time", ) fs.StringVar( @@ -217,21 +217,28 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { emitterServer := emitter.NewServer(loggerEvents) // load keybase - kb, err := setupKeybase(logger.WithGroup(AccountsLogName), cfg) + book, err := setupAddressBook(logger.WithGroup(AccountsLogName), cfg) if err != nil { return fmt.Errorf("unable to load keybase: %w", err) } // Check and Parse packages - pkgpaths, err := resolvePackagesPathFromArgs(cfg, kb, args) + pkgpaths, err := resolvePackagesPathFromArgs(cfg, book, args) if err != nil { return fmt.Errorf("unable to parse package paths: %w", err) } + // generate balances + balances, err := generateBalances(book, cfg) + if err != nil { + return fmt.Errorf("unable to generate balances: %w", err) + } + logger.Debug("balances loaded", "list", balances.List()) + // Setup Dev Node // XXX: find a good way to export or display node logs nodeLogger := logger.WithGroup(NodeLogName) - devNode, err := setupDevNode(ctx, nodeLogger, cfg, emitterServer, kb, pkgpaths) + devNode, err := setupDevNode(ctx, nodeLogger, cfg, emitterServer, balances, pkgpaths) if err != nil { return err } @@ -283,7 +290,7 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { } // Run the main event loop - return runEventLoop(ctx, logger, kb, rt, devNode, watcher) + return runEventLoop(ctx, logger, book, rt, devNode, watcher) } var helper string = ` @@ -297,7 +304,7 @@ Ctrl+C Exit - Exit the application func runEventLoop( ctx context.Context, logger *slog.Logger, - kb keys.Keybase, + bk *address.Book, rt *rawterm.RawTerm, dnode *gnodev.Node, watch *watcher.PackageWatcher, @@ -338,7 +345,7 @@ func runEventLoop( case rawterm.KeyH: // Helper logger.Info("Gno Dev Helper", "helper", helper) case rawterm.KeyA: // Accounts - logAccounts(logger.WithGroup(AccountsLogName), kb, dnode) + logAccounts(logger.WithGroup(AccountsLogName), bk, dnode) case rawterm.KeyR: // Reload logger.WithGroup(NodeLogName).Info("reloading...") if err = dnode.ReloadAll(ctx); err != nil { @@ -380,27 +387,27 @@ func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm. return cc } -func resolvePackagesPathFromArgs(cfg *devCfg, kb keys.Keybase, args []string) ([]gnodev.PackagePath, error) { +func resolvePackagesPathFromArgs(cfg *devCfg, bk *address.Book, args []string) ([]gnodev.PackagePath, error) { paths := make([]gnodev.PackagePath, 0, len(args)) if cfg.deployKey == "" { return nil, fmt.Errorf("default deploy key cannot be empty") } - defaultKey, err := kb.GetByNameOrAddress(cfg.deployKey) - if err != nil { - return nil, fmt.Errorf("unable to get deploy key %q: %w", cfg.deployKey, err) + defaultKey, _, ok := bk.GetFromNameOrAddress(cfg.deployKey) + if !ok { + return nil, fmt.Errorf("unable to get deploy key %q", cfg.deployKey) } for _, arg := range args { - path, err := gnodev.ResolvePackagePathQuery(kb, arg) + path, err := gnodev.ResolvePackagePathQuery(bk, arg) if err != nil { return nil, fmt.Errorf("invalid package path/query %q: %w", arg, err) } // Assign a default creator if user haven't specified it. if path.Creator.IsZero() { - path.Creator = defaultKey.GetAddress() + path.Creator = defaultKey } paths = append(paths, path) @@ -410,7 +417,7 @@ func resolvePackagesPathFromArgs(cfg *devCfg, kb keys.Keybase, args []string) ([ if !cfg.minimal { paths = append(paths, gnodev.PackagePath{ Path: filepath.Join(cfg.root, "examples"), - Creator: defaultKey.GetAddress(), + Creator: defaultKey, Deposit: nil, }) } diff --git a/contribs/gnodev/cmd/gnodev/setup_address_book.go b/contribs/gnodev/cmd/gnodev/setup_address_book.go new file mode 100644 index 00000000000..cf3389fb8d3 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/setup_address_book.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "log/slog" + + "github.com/gnolang/gno/contribs/gnodev/pkg/address" + "github.com/gnolang/gno/tm2/pkg/crypto" + osm "github.com/gnolang/gno/tm2/pkg/os" +) + +func setupAddressBook(logger *slog.Logger, cfg *devCfg) (*address.Book, error) { + book := address.NewBook() + + // Check for home folder + if cfg.home == "" { + logger.Warn("home not specified, no keybase will be loaded") + } else if !osm.DirExists(cfg.home) { + logger.Warn("keybase directory does not exist, no local keys will be imported", + "path", cfg.home) + } else if err := book.ImportKeybase(cfg.home); err != nil { + return nil, fmt.Errorf("unable to import local keybase %q: %w", cfg.home, err) + } + + // Add additional accounts to our keybase + for acc := range cfg.premineAccounts { + if _, ok := book.GetByName(acc); ok { + continue // we already know this account from keybase + } + + // Check if we have a valid bech32 address instead + addr, err := crypto.AddressFromBech32(acc) + if err != nil { + return nil, fmt.Errorf("invalid bech32 address or unkown keyname %q", acc) + } + + book.Add(addr, "") // add addr to the book with no name + + logger.Info("additional account added", "addr", addr.String()) + } + + // Ensure that we have a default address + names, ok := book.GetByAddress(DefaultDeployerAddress) + if ok { + // Account already exist in the keybase + if len(names) > 0 && names[0] != "" { + logger.Info("default address imported", "name", names[0], "addr", DefaultDeployerAddress.String()) + } else { + logger.Info("default address imported", "addr", DefaultDeployerAddress.String()) + } + return book, nil + } + + // If the key isn't found, create a default one + creatorName := fmt.Sprintf("_default#%.6s", DefaultDeployerAddress.String()) + book.Add(DefaultDeployerAddress, creatorName) + + logger.Warn("default address created", + "name", creatorName, + "addr", DefaultDeployerAddress.String(), + "mnemonic", DefaultDeployerSeed, + ) + + return book, nil +} diff --git a/contribs/gnodev/cmd/gnodev/setup_keybase.go b/contribs/gnodev/cmd/gnodev/setup_keybase.go deleted file mode 100644 index bea96293f41..00000000000 --- a/contribs/gnodev/cmd/gnodev/setup_keybase.go +++ /dev/null @@ -1,109 +0,0 @@ -package main - -import ( - "fmt" - "log/slog" - - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/crypto/keys" - "github.com/gnolang/gno/tm2/pkg/crypto/keys/keyerror" - osm "github.com/gnolang/gno/tm2/pkg/os" -) - -func setupKeybase(logger *slog.Logger, cfg *devCfg) (keys.Keybase, error) { - kb := keys.NewInMemory() - - // Check for home folder - if cfg.home == "" { - logger.Warn("local keybase disabled") - } else if !osm.DirExists(cfg.home) { - logger.Warn("keybase directory does not exist, no local keys will be imported", - "path", cfg.home) - } else if err := importKeybase(kb, cfg.home); err != nil { - return nil, fmt.Errorf("unable to import local keybase %q: %w", cfg.home, err) - } - - // Add additional accounts to our keybase - for acc := range cfg.additionalAccounts { - // Check if the account exist in the local keybase - if ok, _ := kb.HasByName(acc); ok { - continue - } - - // Check if we have a valid bech32 address instead - addr, err := crypto.AddressFromBech32(acc) - if err != nil { - return nil, fmt.Errorf("invalid bech32 address or unkown key %q", acc) - } - - // If we already know this address from keybase, skip it - ok, err := kb.HasByAddress(addr) - if ok { - continue - } - - // We don't know this address, then add it to our keybase - pub, err := crypto.PubKeyFromBech32(acc) - if err != nil { - return nil, fmt.Errorf("unable to get PubKey from %q: %w", acc, err) - } - - name := fmt.Sprintf("_account#%.6s", addr.String()) - info, err := kb.CreateOffline(name, pub) - if err != nil { - return nil, fmt.Errorf("unable to add additional account: %w", err) - } - - logger.Info("additional account added", - "name", info.GetName(), - "addr", info.GetAddress()) - } - - // Ensure that we have a default address - info, err := kb.GetByAddress(DefaultDeployerAddress) - switch { - case err == nil: // Account already exist in the keybase - logger.Info("default address imported from keybase", "name", info.GetName(), "addr", info.GetAddress()) - case keyerror.IsErrKeyNotFound(err): - // If the key isn't found, create a default one - creatorName := fmt.Sprintf("_default#%.6s", DefaultDeployerAddress.String()) - if ok, _ := kb.HasByName(creatorName); ok { - return nil, fmt.Errorf("unable to create default account, %q already exist in imported keybase", creatorName) - } - - info, err = kb.CreateAccount(creatorName, DefaultDeployerSeed, "", "", 0, 0) - if err != nil { - return nil, fmt.Errorf("unable to create default account %q: %w", DefaultDeployerName, err) - } - - logger.Warn("default address created", - "name", info.GetName(), - "addr", info.GetAddress(), - "mnemonic", DefaultDeployerSeed, - ) - default: - return nil, fmt.Errorf("unable to get address %q: %w", info.GetAddress(), err) - } - - return kb, nil -} - -func importKeybase(to keys.Keybase, path string) error { - // Load home keybase into our inMemory keybase - from, err := keys.NewKeyBaseFromDir(path) - if err != nil { - return fmt.Errorf("unable to load keybase: %w", err) - } - - keys, err := from.List() - if err != nil { - return fmt.Errorf("unable to list keys: %w", err) - } - - for _, key := range keys { - name := key.GetName() - to.CreateOffline(name, key.GetPubKey()) - } - - return nil -} diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index dde94c843d2..c79ab9d18bf 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -10,7 +10,6 @@ import ( gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/crypto/keys" ) // setupDevNode initializes and returns a new DevNode. @@ -19,15 +18,9 @@ func setupDevNode( logger *slog.Logger, cfg *devCfg, remitter emitter.Emitter, - kb keys.Keybase, + balances gnoland.Balances, pkgspath []gnodev.PackagePath, ) (*gnodev.Node, error) { - balances, err := generateBalances(kb, cfg) - if err != nil { - return nil, fmt.Errorf("unable to generate balances: %w", err) - } - logger.Debug("balances loaded", "list", balances.List()) - config := setupDevNodeConfig(cfg, balances, pkgspath) return gnodev.NewDevNode(ctx, logger, remitter, config) } diff --git a/contribs/gnodev/pkg/address/book.go b/contribs/gnodev/pkg/address/book.go new file mode 100644 index 00000000000..e6ad0d75f5e --- /dev/null +++ b/contribs/gnodev/pkg/address/book.go @@ -0,0 +1,133 @@ +package address + +import ( + "fmt" + "sort" + + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" +) + +type Entry struct { + crypto.Address + Names []string +} + +type Book struct { + addrsToNames map[crypto.Address][]string // address -> []names + namesToAddrs map[string]crypto.Address // name -> address +} + +func NewBook() *Book { + return &Book{ + addrsToNames: map[crypto.Address][]string{}, + namesToAddrs: map[string]crypto.Address{}, + } +} + +func remove(s []string, i int) []string { + s[len(s)-1], s[i] = s[i], s[len(s)-1] + return s[:len(s)-1] +} + +func (bk *Book) Add(addr crypto.Address, name string) { + // Check and register address if it wasn't existing + names, ok := bk.addrsToNames[addr] + if !ok { + bk.addrsToNames[addr] = []string{} + } + + // If name is empty, stop here + if name == "" { + return + } + + oldAddr, ok := bk.namesToAddrs[name] + if !ok { + bk.namesToAddrs[name] = addr + bk.addrsToNames[addr] = append(names, name) + return + } + + // Check if the association already exist + if oldAddr.Compare(addr) == 0 { + return // nothing to do + } + + // If the name is associated with a different address, remove the old association + oldNames := bk.addrsToNames[oldAddr] + for i, oldName := range oldNames { + if oldName == name { + bk.addrsToNames[oldAddr] = remove(oldNames, i) + break + } + } + + // Add the new association + bk.namesToAddrs[name] = addr + bk.addrsToNames[addr] = append(names, name) +} + +func (bk Book) List() []Entry { + entries := make([]Entry, 0, len(bk.addrsToNames)) + for addr, names := range bk.addrsToNames { + namesCopy := make([]string, len(names)) + copy(namesCopy, names) + + newEntry := Entry{ + Address: addr, + Names: namesCopy, + } + + // Find the correct place to insert newEntry using binary search. + i := sort.Search(len(entries), func(i int) bool { + return entries[i].Address.Compare(newEntry.Address) >= 0 + }) + + entries = append(entries[:i], append([]Entry{newEntry}, entries[i:]...)...) + } + + return entries +} + +func (bk Book) GetByAddress(addr crypto.Address) (names []string, ok bool) { + names, ok = bk.addrsToNames[addr] + return +} + +func (bk Book) GetByName(name string) (addr crypto.Address, ok bool) { + addr, ok = bk.namesToAddrs[name] + return +} + +func (bk Book) GetFromNameOrAddress(addrOrName string) (addr crypto.Address, names []string, ok bool) { + var err error + if addr, ok = bk.namesToAddrs[addrOrName]; ok { + names = []string{addrOrName} + } else if addr, err = crypto.AddressFromBech32(addrOrName); err == nil { + // addr is valid, now check if we have it + names, ok = bk.addrsToNames[addr] + } + + return +} + +func (bk Book) ImportKeybase(path string) error { + kb, err := keys.NewKeyBaseFromDir(path) + if err != nil { + return fmt.Errorf("unable to load keybase: %w", err) + } + defer kb.CloseDB() + + keys, err := kb.List() + if err != nil { + return fmt.Errorf("unable to list keys: %w", err) + } + + for _, key := range keys { + name := key.GetName() + bk.Add(key.GetAddress(), name) + } + + return nil +} diff --git a/contribs/gnodev/pkg/address/book_test.go b/contribs/gnodev/pkg/address/book_test.go new file mode 100644 index 00000000000..45dca7df836 --- /dev/null +++ b/contribs/gnodev/pkg/address/book_test.go @@ -0,0 +1,109 @@ +package address + +import ( + "testing" + + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/jaekwon/testify/require" + "github.com/stretchr/testify/assert" +) + +var testAddr = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + +func TestNewBook(t *testing.T) { + t.Parallel() + + bk := NewBook() + assert.Empty(t, bk.addrsToNames) + assert.Empty(t, bk.namesToAddrs) +} + +func TestAddEmptyName(t *testing.T) { + t.Parallel() + + bk := NewBook() + + // Add address + bk.Add(testAddr, "") + + names, ok := bk.GetByAddress(testAddr) + require.True(t, ok) + require.Equal(t, 0, len(names)) +} + +func TestAdd(t *testing.T) { + t.Parallel() + + bk := NewBook() + + // Add address + bk.Add(testAddr, "testname") + + t.Run("get by address", func(t *testing.T) { + names, ok := bk.GetByAddress(testAddr) + require.True(t, ok) + require.Equal(t, 1, len(names)) + assert.Equal(t, "testname", names[0]) + }) + + t.Run("get by name", func(t *testing.T) { + addrFromName, ok := bk.GetByName("testname") + assert.True(t, ok) + assert.True(t, addrFromName.Compare(testAddr) == 0) + }) + + // Add same address with a new name + bk.Add(testAddr, "testname2") + + t.Run("get two names with same address", func(t *testing.T) { + // Get by name + addr1, ok := bk.GetByName("testname") + require.True(t, ok) + addr2, ok := bk.GetByName("testname2") + require.True(t, ok) + assert.True(t, addr1.Compare(addr2) == 0) + }) +} + +func TestList(t *testing.T) { + t.Parallel() + + bk := NewBook() + + bk.Add(testAddr, "testname") + + entries := bk.List() + + assert.Equal(t, 1, len(entries)) + entry := entries[0] + + assert.True(t, testAddr.Compare(entry.Address) == 0) + assert.Equal(t, 1, len(entries[0].Names)) + assert.Equal(t, "testname", entries[0].Names[0]) +} + +func TestGetFromNameOrAddress(t *testing.T) { + t.Parallel() + + bk := NewBook() + + t.Run("failure", func(t *testing.T) { + resultAddr, names, ok := bk.GetFromNameOrAddress("unkown_key") + assert.False(t, ok) + assert.True(t, resultAddr.IsZero()) + assert.Len(t, names, 0) + }) + + // Add address + bk.Add(testAddr, "testname") + + for _, addrOrName := range []string{"testname", testAddr.String()} { + t.Run(addrOrName, func(t *testing.T) { + resultAddr, names, ok := bk.GetFromNameOrAddress("testname") + require.True(t, ok) + require.Len(t, names, 1) + assert.Equal(t, "testname", names[0]) + assert.True(t, resultAddr.Compare(testAddr) == 0) + }) + } +} diff --git a/contribs/gnodev/pkg/dev/packages.go b/contribs/gnodev/pkg/dev/packages.go index 0dd67e61ac4..f58775277e9 100644 --- a/contribs/gnodev/pkg/dev/packages.go +++ b/contribs/gnodev/pkg/dev/packages.go @@ -6,11 +6,11 @@ import ( "net/url" "path/filepath" + "github.com/gnolang/gno/contribs/gnodev/pkg/address" 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" "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -20,7 +20,7 @@ type PackagePath struct { Deposit std.Coins } -func ResolvePackagePathQuery(kb keys.Keybase, path string) (PackagePath, error) { +func ResolvePackagePathQuery(bk *address.Book, path string) (PackagePath, error) { var ppath PackagePath upath, err := url.Parse(path) @@ -34,12 +34,11 @@ func ResolvePackagePathQuery(kb keys.Keybase, path string) (PackagePath, error) if creator != "" { address, err := crypto.AddressFromBech32(creator) if err != nil { - info, nameErr := kb.GetByName(creator) - if nameErr != nil { + var ok bool + address, ok = bk.GetByName(creator) + if !ok { return ppath, fmt.Errorf("invalid name or address for creator %q", creator) } - - address = info.GetAddress() } ppath.Creator = address From f2593b02c73915a1014f87c29c2b442203d9ed47 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:38:42 +0200 Subject: [PATCH 16/23] fix: lint error Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/accounts.go | 1 - contribs/gnodev/cmd/gnodev/setup_address_book.go | 2 +- contribs/gnodev/pkg/address/book_test.go | 10 +++++++++- contribs/gnodev/pkg/dev/packages_test.go | 13 ++++++------- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/contribs/gnodev/cmd/gnodev/accounts.go b/contribs/gnodev/cmd/gnodev/accounts.go index e725aa1017d..1b695d6e1d0 100644 --- a/contribs/gnodev/cmd/gnodev/accounts.go +++ b/contribs/gnodev/cmd/gnodev/accounts.go @@ -73,7 +73,6 @@ func generateBalances(bk *address.Book, cfg *devCfg) (gnoland.Balances, error) { found = preDefinedFound break } - } bls[address] = gnoland.Balance{Amount: found, Address: address} diff --git a/contribs/gnodev/cmd/gnodev/setup_address_book.go b/contribs/gnodev/cmd/gnodev/setup_address_book.go index cf3389fb8d3..a1a1c8f58ac 100644 --- a/contribs/gnodev/cmd/gnodev/setup_address_book.go +++ b/contribs/gnodev/cmd/gnodev/setup_address_book.go @@ -31,7 +31,7 @@ func setupAddressBook(logger *slog.Logger, cfg *devCfg) (*address.Book, error) { // Check if we have a valid bech32 address instead addr, err := crypto.AddressFromBech32(acc) if err != nil { - return nil, fmt.Errorf("invalid bech32 address or unkown keyname %q", acc) + return nil, fmt.Errorf("invalid bech32 address or unknown keyname %q", acc) } book.Add(addr, "") // add addr to the book with no name diff --git a/contribs/gnodev/pkg/address/book_test.go b/contribs/gnodev/pkg/address/book_test.go index 45dca7df836..de177a2a714 100644 --- a/contribs/gnodev/pkg/address/book_test.go +++ b/contribs/gnodev/pkg/address/book_test.go @@ -40,6 +40,8 @@ func TestAdd(t *testing.T) { bk.Add(testAddr, "testname") t.Run("get by address", func(t *testing.T) { + t.Parallel() + names, ok := bk.GetByAddress(testAddr) require.True(t, ok) require.Equal(t, 1, len(names)) @@ -47,6 +49,8 @@ func TestAdd(t *testing.T) { }) t.Run("get by name", func(t *testing.T) { + t.Parallel() + addrFromName, ok := bk.GetByName("testname") assert.True(t, ok) assert.True(t, addrFromName.Compare(testAddr) == 0) @@ -56,6 +60,8 @@ func TestAdd(t *testing.T) { bk.Add(testAddr, "testname2") t.Run("get two names with same address", func(t *testing.T) { + t.Parallel() + // Get by name addr1, ok := bk.GetByName("testname") require.True(t, ok) @@ -88,7 +94,9 @@ func TestGetFromNameOrAddress(t *testing.T) { bk := NewBook() t.Run("failure", func(t *testing.T) { - resultAddr, names, ok := bk.GetFromNameOrAddress("unkown_key") + t.Parallel() + + resultAddr, names, ok := bk.GetFromNameOrAddress("unknown_key") assert.False(t, ok) assert.True(t, resultAddr.IsZero()) assert.Len(t, names, 0) diff --git a/contribs/gnodev/pkg/dev/packages_test.go b/contribs/gnodev/pkg/dev/packages_test.go index b262617c09f..760d3cdef80 100644 --- a/contribs/gnodev/pkg/dev/packages_test.go +++ b/contribs/gnodev/pkg/dev/packages_test.go @@ -3,8 +3,8 @@ package dev import ( "testing" + "github.com/gnolang/gno/contribs/gnodev/pkg/address" "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/std" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -12,13 +12,12 @@ import ( func TestResolvePackagePathQuery(t *testing.T) { var ( - testingName = "testAccount" - testingMnemonic = `special hip mail knife manual boy essay certain broccoli group token exchange problem subject garbage chaos program monitor happy magic upgrade kingdom cluster enemy` - testingAddress = crypto.MustAddressFromString("g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na") + testingName = "testAccount" + testingAddress = crypto.MustAddressFromString("g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na") ) - kb := keys.NewInMemory() - kb.CreateAccount(testingName, testingMnemonic, "", "", 0, 0) + book := address.NewBook() + book.Add(testingAddress, testingName) cases := []struct { Path string @@ -56,7 +55,7 @@ func TestResolvePackagePathQuery(t *testing.T) { for _, tc := range cases { t.Run(tc.Path, func(t *testing.T) { - result, err := ResolvePackagePathQuery(kb, tc.Path) + result, err := ResolvePackagePathQuery(book, tc.Path) if tc.ShouldFail { assert.Error(t, err) return From 0abe9266c7fcd00673bb8dc93939617a99f20701 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:42:43 +0200 Subject: [PATCH 17/23] chore: move balance types in its own file Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/gnoland/balance.go | 150 ++++++++++++++++++ .../{types_test.go => balance_test.go} | 0 gno.land/pkg/gnoland/types.go | 144 ----------------- 3 files changed, 150 insertions(+), 144 deletions(-) create mode 100644 gno.land/pkg/gnoland/balance.go rename gno.land/pkg/gnoland/{types_test.go => balance_test.go} (100%) diff --git a/gno.land/pkg/gnoland/balance.go b/gno.land/pkg/gnoland/balance.go new file mode 100644 index 00000000000..807da4cf41f --- /dev/null +++ b/gno.land/pkg/gnoland/balance.go @@ -0,0 +1,150 @@ +package gnoland + +import ( + "bufio" + "fmt" + "io" + "strings" + + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type Balance struct { + Address bft.Address + Amount std.Coins +} + +func (b *Balance) Verify() error { + if b.Address.IsZero() { + return ErrBalanceEmptyAddress + } + + if b.Amount.Len() == 0 { + return ErrBalanceEmptyAmount + } + + return nil +} + +func (b *Balance) Parse(entry string) error { + parts := strings.Split(strings.TrimSpace(entry), "=") //
= + if len(parts) != 2 { + return fmt.Errorf("malformed entry: %q", entry) + } + + var err error + + b.Address, err = crypto.AddressFromBech32(parts[0]) + if err != nil { + return fmt.Errorf("invalid address %q: %w", parts[0], err) + } + + b.Amount, err = std.ParseCoins(parts[1]) + if err != nil { + return fmt.Errorf("invalid amount %q: %w", parts[1], err) + } + + return nil +} + +func (b *Balance) UnmarshalAmino(rep string) error { + return b.Parse(rep) +} + +func (b Balance) MarshalAmino() (string, error) { + return b.String(), nil +} + +func (b Balance) String() string { + return fmt.Sprintf("%s=%s", b.Address.String(), b.Amount.String()) +} + +type Balances map[crypto.Address]Balance + +func NewBalances() Balances { + return make(Balances) +} + +func (bs Balances) Set(address crypto.Address, amount std.Coins) { + bs[address] = Balance{ + Address: address, + Amount: amount, + } +} + +func (bs Balances) Get(address crypto.Address) (balance Balance, ok bool) { + balance, ok = bs[address] + return +} + +func (bs Balances) List() []Balance { + list := make([]Balance, 0, len(bs)) + for _, balance := range bs { + list = append(list, balance) + } + return list +} + +// LeftMerge left-merges the two maps +func (bs Balances) LeftMerge(from Balances) { + for key, bVal := range from { + if _, present := (bs)[key]; !present { + (bs)[key] = bVal + } + } +} + +func GetBalancesFromEntries(entries ...string) (Balances, error) { + balances := NewBalances() + return balances, balances.LoadFromEntries(entries...) +} + +// LoadFromEntries extracts the balance entries in the form of
= +func (bs Balances) LoadFromEntries(entries ...string) error { + for _, entry := range entries { + var balance Balance + if err := balance.Parse(entry); err != nil { + return fmt.Errorf("unable to parse balance entry: %w", err) + } + bs[balance.Address] = balance + } + + return nil +} + +func GetBalancesFromSheet(sheet io.Reader) (Balances, error) { + balances := NewBalances() + return balances, balances.LoadFromSheet(sheet) +} + +// LoadFromSheet extracts the balance sheet from the passed in +// balance sheet file, that has the format of
=ugnot +func (bs Balances) LoadFromSheet(sheet io.Reader) error { + // Parse the balances + scanner := bufio.NewScanner(sheet) + + for scanner.Scan() { + entry := scanner.Text() + + // Remove comments + entry = strings.Split(entry, "#")[0] + entry = strings.TrimSpace(entry) + + // Skip empty lines + if entry == "" { + continue + } + + if err := bs.LoadFromEntries(entry); err != nil { + return fmt.Errorf("unable to load entries: %w", err) + } + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("error encountered while scanning, %w", err) + } + + return nil +} diff --git a/gno.land/pkg/gnoland/types_test.go b/gno.land/pkg/gnoland/balance_test.go similarity index 100% rename from gno.land/pkg/gnoland/types_test.go rename to gno.land/pkg/gnoland/balance_test.go diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index 36ea3384c3c..016f3279dbd 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -1,14 +1,8 @@ package gnoland import ( - "bufio" "errors" - "fmt" - "io" - "strings" - bft "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -29,141 +23,3 @@ type GnoGenesisState struct { Balances []Balance `json:"balances"` Txs []std.Tx `json:"txs"` } - -type Balance struct { - Address bft.Address - Amount std.Coins -} - -func (b *Balance) Verify() error { - if b.Address.IsZero() { - return ErrBalanceEmptyAddress - } - - if b.Amount.Len() == 0 { - return ErrBalanceEmptyAmount - } - - return nil -} - -func (b *Balance) Parse(entry string) error { - parts := strings.Split(strings.TrimSpace(entry), "=") //
= - if len(parts) != 2 { - return fmt.Errorf("malformed entry: %q", entry) - } - - var err error - - b.Address, err = crypto.AddressFromBech32(parts[0]) - if err != nil { - return fmt.Errorf("invalid address %q: %w", parts[0], err) - } - - b.Amount, err = std.ParseCoins(parts[1]) - if err != nil { - return fmt.Errorf("invalid amount %q: %w", parts[1], err) - } - - return nil -} - -func (b *Balance) UnmarshalAmino(rep string) error { - return b.Parse(rep) -} - -func (b Balance) MarshalAmino() (string, error) { - return b.String(), nil -} - -func (b Balance) String() string { - return fmt.Sprintf("%s=%s", b.Address.String(), b.Amount.String()) -} - -type Balances map[crypto.Address]Balance - -func NewBalances() Balances { - return make(Balances) -} - -func (bs Balances) Set(address crypto.Address, amount std.Coins) { - bs[address] = Balance{ - Address: address, - Amount: amount, - } -} - -func (bs Balances) Get(address crypto.Address) (balance Balance, ok bool) { - balance, ok = bs[address] - return -} - -func (bs Balances) List() []Balance { - list := make([]Balance, 0, len(bs)) - for _, balance := range bs { - list = append(list, balance) - } - return list -} - -// leftMerge left-merges the two maps -func (bs Balances) LeftMerge(from Balances) { - for key, bVal := range from { - if _, present := (bs)[key]; !present { - (bs)[key] = bVal - } - } -} - -func GetBalancesFromEntries(entries ...string) (Balances, error) { - balances := NewBalances() - return balances, balances.LoadFromEntries(entries...) -} - -// LoadFromEntries extracts the balance entries in the form of
= -func (bs Balances) LoadFromEntries(entries ...string) error { - for _, entry := range entries { - var balance Balance - if err := balance.Parse(entry); err != nil { - return fmt.Errorf("unable to parse balance entry: %w", err) - } - bs[balance.Address] = balance - } - - return nil -} - -func GetBalancesFromSheet(sheet io.Reader) (Balances, error) { - balances := NewBalances() - return balances, balances.LoadFromSheet(sheet) -} - -// LoadFromSheet extracts the balance sheet from the passed in -// balance sheet file, that has the format of
=ugnot -func (bs Balances) LoadFromSheet(sheet io.Reader) error { - // Parse the balances - scanner := bufio.NewScanner(sheet) - - for scanner.Scan() { - entry := scanner.Text() - - // Remove comments - entry = strings.Split(entry, "#")[0] - entry = strings.TrimSpace(entry) - - // Skip empty lines - if entry == "" { - continue - } - - if err := bs.LoadFromEntries(entry); err != nil { - return fmt.Errorf("unable to load entries: %w", err) - } - } - - if err := scanner.Err(); err != nil { - return fmt.Errorf("error encountered while scanning, %w", err) - } - - return nil -} From e4ac3c118a6089c4781ac268117a0f9182188749 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:43:26 +0200 Subject: [PATCH 18/23] chore: add parallel to some tests Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/pkg/dev/packages_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contribs/gnodev/pkg/dev/packages_test.go b/contribs/gnodev/pkg/dev/packages_test.go index 760d3cdef80..dbae99b4484 100644 --- a/contribs/gnodev/pkg/dev/packages_test.go +++ b/contribs/gnodev/pkg/dev/packages_test.go @@ -11,6 +11,8 @@ import ( ) func TestResolvePackagePathQuery(t *testing.T) { + t.Parallel() + var ( testingName = "testAccount" testingAddress = crypto.MustAddressFromString("g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na") From 5e248a3f51ffaaa9ea6d09539201426b2f3d3273 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:50:29 +0200 Subject: [PATCH 19/23] chore: update doc Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/main.go | 2 +- docs/gno-tooling/cli/gnodev.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 0f3e962feaf..652b4a862a3 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -128,7 +128,7 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { fs.Var( &c.premineAccounts, "add-account", - "add (or set) a premine account in the form `[:]`, can be used multiple time", + "add (or set) a premine account in the form `[=]`, can be used multiple time", ) fs.StringVar( diff --git a/docs/gno-tooling/cli/gnodev.md b/docs/gno-tooling/cli/gnodev.md index 74d60739f0c..97a4cc2e5ce 100644 --- a/docs/gno-tooling/cli/gnodev.md +++ b/docs/gno-tooling/cli/gnodev.md @@ -109,7 +109,7 @@ While `gnodev` is running, the following shortcuts are available: |---------------------|------------------------------------------------------------| | --minimal | Start `gnodev` without loading the examples folder. | | --no-watch | Disable hot reload. | -| --add-account | Pre-add account(s) in the form `[:]` | +| --add-account | Pre-add account(s) in the form `[=]` | | --balances-file | Load a balance for the user(s) from a balance file. | | --chain-id | Set node ChainID | | --deploy-key | Default key name or Bech32 address for uploading packages. | From 2442f1ea04743c899f2a3ac9d02f16a65ceb3939 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:55:21 +0200 Subject: [PATCH 20/23] chore: remove testify jaekwon deps from bad merge Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/go.mod | 2 +- contribs/gnodev/pkg/address/book_test.go | 2 +- gno.land/pkg/gnoland/balance_test.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index 90bef4b2b13..8b66f72d288 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -11,6 +11,7 @@ require ( github.com/gnolang/gno v0.0.0-00010101000000-000000000000 github.com/gorilla/websocket v1.5.1 github.com/muesli/termenv v0.15.2 + github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 golang.org/x/term v0.18.0 ) @@ -48,7 +49,6 @@ require ( github.com/rivo/uniseg v0.4.3 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rs/cors v1.10.1 // indirect - github.com/stretchr/testify v1.9.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect diff --git a/contribs/gnodev/pkg/address/book_test.go b/contribs/gnodev/pkg/address/book_test.go index de177a2a714..80249762455 100644 --- a/contribs/gnodev/pkg/address/book_test.go +++ b/contribs/gnodev/pkg/address/book_test.go @@ -4,8 +4,8 @@ import ( "testing" "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/jaekwon/testify/require" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var testAddr = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") diff --git a/gno.land/pkg/gnoland/balance_test.go b/gno.land/pkg/gnoland/balance_test.go index 0cc2424fc7e..63e80e3b41b 100644 --- a/gno.land/pkg/gnoland/balance_test.go +++ b/gno.land/pkg/gnoland/balance_test.go @@ -15,8 +15,8 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" "github.com/gnolang/gno/tm2/pkg/std" - "github.com/jaekwon/testify/assert" - "github.com/jaekwon/testify/require" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestBalance_Verify(t *testing.T) { From 766585ca13a3be1ec88562a5def286a07481ed18 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 23 Apr 2024 17:06:16 +0200 Subject: [PATCH 21/23] fix: balances tests Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/gnoland/balance_test.go | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/gno.land/pkg/gnoland/balance_test.go b/gno.land/pkg/gnoland/balance_test.go index 63e80e3b41b..59dffcc4333 100644 --- a/gno.land/pkg/gnoland/balance_test.go +++ b/gno.land/pkg/gnoland/balance_test.go @@ -142,9 +142,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { } balanceMap, err := GetBalancesFromEntries(entries...) - require.NoError(t, err) - - assert.Nil(t, balanceMap) + assert.Len(t, balanceMap, 0) assert.Contains(t, err.Error(), "malformed entry") }) @@ -156,10 +154,8 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { } balanceMap, err := GetBalancesFromEntries(balances...) - require.NoError(t, err) - - assert.Nil(t, balanceMap) - assert.Contains(t, err.Error(), "invalid address") + assert.Len(t, balanceMap, 0) + assert.ErrorContains(t, err, "invalid address") }) t.Run("malformed balance, invalid amount", func(t *testing.T) { @@ -176,9 +172,8 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { } balanceMap, err := GetBalancesFromEntries(balances...) - - assert.Nil(t, balanceMap) - assert.Contains(t, err.Error(), "invalid amount") + assert.Len(t, balanceMap, 0) + assert.ErrorContains(t, err, "invalid amount") }) } @@ -230,7 +225,7 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) { balanceMap, err := GetBalancesFromSheet(reader) - assert.Nil(t, balanceMap) + assert.Len(t, balanceMap, 0) assert.Contains(t, err.Error(), "invalid amount") }) } From 1f52b90ace633319e30dfbe04ec4eb2fb4a13f14 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 23 Apr 2024 17:23:03 +0200 Subject: [PATCH 22/23] chore: rename unlimitedFund -> premineBalance Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/accounts.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contribs/gnodev/cmd/gnodev/accounts.go b/contribs/gnodev/cmd/gnodev/accounts.go index 1b695d6e1d0..b263cc44f70 100644 --- a/contribs/gnodev/cmd/gnodev/accounts.go +++ b/contribs/gnodev/cmd/gnodev/accounts.go @@ -50,7 +50,7 @@ func (va varPremineAccounts) String() string { func generateBalances(bk *address.Book, cfg *devCfg) (gnoland.Balances, error) { bls := gnoland.NewBalances() - unlimitedFund := std.Coins{std.NewCoin("ugnot", 10e12)} + premineBalance := std.Coins{std.NewCoin("ugnot", 10e12)} entries := bk.List() @@ -67,7 +67,7 @@ func generateBalances(bk *address.Book, cfg *devCfg) (gnoland.Balances, error) { } // Check for name - found := unlimitedFund + found := premineBalance for _, name := range entry.Names { if preDefinedFound, ok := cfg.premineAccounts[name]; ok && preDefinedFound != nil { found = preDefinedFound From 55b863f64a5644f0249a15750e6429174834019c Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:34:07 +0200 Subject: [PATCH 23/23] chore: improve book readability Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/pkg/address/book.go | 31 +++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/contribs/gnodev/pkg/address/book.go b/contribs/gnodev/pkg/address/book.go index e6ad0d75f5e..983fced5882 100644 --- a/contribs/gnodev/pkg/address/book.go +++ b/contribs/gnodev/pkg/address/book.go @@ -8,11 +8,8 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto/keys" ) -type Entry struct { - crypto.Address - Names []string -} - +// Book reference a list of addresses optionally associated with a name +// It is not thread safe. type Book struct { addrsToNames map[crypto.Address][]string // address -> []names namesToAddrs map[string]crypto.Address // name -> address @@ -25,12 +22,16 @@ func NewBook() *Book { } } -func remove(s []string, i int) []string { - s[len(s)-1], s[i] = s[i], s[len(s)-1] - return s[:len(s)-1] -} - +// Add inserts a new address into the address book linked to the specified name. +// An address can be associated with multiple names, yet each name can only +// belong to one address. Hence, if a name is reused, it will replace the +// reference to the previous address. +// Adding an address without a name is permissible. func (bk *Book) Add(addr crypto.Address, name string) { + if addr.IsZero() { + panic("empty address not allowed") + } + // Check and register address if it wasn't existing names, ok := bk.addrsToNames[addr] if !ok { @@ -68,6 +69,11 @@ func (bk *Book) Add(addr crypto.Address, name string) { bk.addrsToNames[addr] = append(names, name) } +type Entry struct { + crypto.Address + Names []string +} + func (bk Book) List() []Entry { entries := make([]Entry, 0, len(bk.addrsToNames)) for addr, names := range bk.addrsToNames { @@ -131,3 +137,8 @@ func (bk Book) ImportKeybase(path string) error { return nil } + +func remove(s []string, i int) []string { + s[len(s)-1], s[i] = s[i], s[len(s)-1] + return s[:len(s)-1] +}