Skip to content

Commit

Permalink
kwil-cli,pkg/client: add --nonce override flag to database cmds
Browse files Browse the repository at this point in the history
This adds the ability to set a nonce when making a transaction.

Rather than giving every method a nonce input arg, I've added a
WithNonceFunc option to pkg/client.Client. This is perhaps a
little strange but the cascade from a new argument was rather
large, and it seemed a little out of place as the other arguments
pertain to the action rather than the transaction containing it.
Can be switched if we'd prefer.
  • Loading branch information
jchappelow committed Oct 5, 2023
1 parent 5ea86d3 commit da75a9e
Show file tree
Hide file tree
Showing 19 changed files with 87 additions and 44 deletions.
11 changes: 0 additions & 11 deletions cmd/kwil-cli/cmds/common/prompt/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"strings"

"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
)

type Prompter struct {
Expand Down Expand Up @@ -50,13 +49,3 @@ func (p Prompter) Run() (string, error) {

return prompt.Run()
}

func ConfirmPrompt() bool {
prompt := promptui.Select{
Label: "Are you sure?",
Items: []string{"Apply", "Abort"},
}
_, result, err := prompt.Run()
cobra.CheckErr(err)
return result == "Apply"
}
8 changes: 7 additions & 1 deletion cmd/kwil-cli/cmds/common/roundtripper.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const (

type RoundTripper func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error

func DialClient(ctx context.Context, flags uint8, fn RoundTripper) error {
func DialClient(ctx context.Context, flags uint8, nonce int64, fn RoundTripper) error {
conf, err := config.LoadCliConfig()
if err != nil {
return err
Expand All @@ -41,6 +41,12 @@ func DialClient(ctx context.Context, flags uint8, fn RoundTripper) error {
options = append(options, client.WithSigner(&signer))
}

if nonce >= 0 {
options = append(options, client.WithNonceFunc(func(context.Context) (uint64, error) {
return uint64(nonce), nil
}))
}

if conf.GrpcURL == "" {
// the grpc url is required
// this is somewhat redundant since the config marks it as required, but in case the config is changed
Expand Down
2 changes: 1 addition & 1 deletion cmd/kwil-cli/cmds/database/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The execution is treated as a single transaction, and will either succeed or fai
RunE: func(cmd *cobra.Command, args []string) error {
var resp []byte

err := common.DialClient(cmd.Context(), 0, func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error {
err := common.DialClient(cmd.Context(), 0, nonceOverride, func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error {
dbid, err := getSelectedDbid(cmd, conf)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/kwil-cli/cmds/database/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ OR
RunE: func(cmd *cobra.Command, args []string) error {
var resp respRelations

err := common.DialClient(cmd.Context(), 0, func(ctx context.Context, clnt *client.Client, conf *config.KwilCliConfig) error {
err := common.DialClient(cmd.Context(), 0, -1, func(ctx context.Context, clnt *client.Client, conf *config.KwilCliConfig) error {
dbid, err := getSelectedDbid(cmd, conf)
if err != nil {
return fmt.Errorf("target database not properly specified: %w", err)
Expand Down
31 changes: 23 additions & 8 deletions cmd/kwil-cli/cmds/database/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,40 @@ import (
)

var (
rootCmd = &cobra.Command{
dbCmd = &cobra.Command{
Use: "database",
Aliases: []string{"db"},
Short: "manage databases",
Long: "Database is a command that contains subcommands for interacting with databases",
}

nonceOverride int64
)

func NewCmdDatabase() *cobra.Command {
rootCmd.AddCommand(
// readOnlyCmds do not create a transaction.
readOnlyCmds := []*cobra.Command{
listCmd(),
readSchemaCmd(),
queryCmd(),
callCmd(), // no tx, but may required key for signature, for now
}
dbCmd.AddCommand(readOnlyCmds...)

// writeCmds create a transactions, requiring a private key for signing/
writeCmds := []*cobra.Command{
deployCmd(),
dropCmd(),
readSchemaCmd(),
executeCmd(),
listCmd(),
batchCmd(),
queryCmd(),
callCmd(),
)
}
dbCmd.AddCommand(writeCmds...)

// The write commands may also specify a nonce to use instead of asking the
// node for the latest confirmed nonce.
for _, cmd := range writeCmds {
cmd.Flags().Int64VarP(&nonceOverride, "nonce", "N", -1, "nonce override (-1 means request from server)")
}

return rootCmd
return dbCmd
}
2 changes: 1 addition & 1 deletion cmd/kwil-cli/cmds/database/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func deployCmd() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
var txHash []byte

err := common.DialClient(cmd.Context(), 0, func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error {
err := common.DialClient(cmd.Context(), 0, nonceOverride, func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error {
// read in the file
file, err := os.Open(filePath)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/kwil-cli/cmds/database/drop.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func dropCmd() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
var resp []byte

err := common.DialClient(cmd.Context(), 0, func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error {
err := common.DialClient(cmd.Context(), 0, nonceOverride, func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error {
var _err error
resp, _err = client.DropDatabase(ctx, args[0])
if _err != nil {
Expand Down
4 changes: 3 additions & 1 deletion cmd/kwil-cli/cmds/database/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ OR
RunE: func(cmd *cobra.Command, args []string) error {
var resp []byte

err := common.DialClient(cmd.Context(), 0, func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error {
err := common.DialClient(cmd.Context(), 0, nonceOverride, func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error {
dbId, err := getSelectedDbid(cmd, conf)
if err != nil {
return fmt.Errorf("target database not properly specified: %w", err)
Expand All @@ -61,6 +61,8 @@ OR
return fmt.Errorf("error getting inputs: %w", err)
}

// Could actually just directly pass nonce to the client method,
// but those methods don't need tx details in the inputs.
resp, err = client.ExecuteAction(ctx, dbId, lowerName, inputs...)
if err != nil {
return fmt.Errorf("error executing database: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kwil-cli/cmds/database/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ A wallet can be specified with the --owner flag, otherwise the default wallet is
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
var resp *respDBList
err := common.DialClient(cmd.Context(), common.WithoutPrivateKey, func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error {
err := common.DialClient(cmd.Context(), common.WithoutPrivateKey, -1, func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error {
owner, err := getSelectedOwner(cmd, conf)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/kwil-cli/cmds/database/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func queryCmd() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
var resp respRelations

err := common.DialClient(cmd.Context(), common.WithoutPrivateKey,
err := common.DialClient(cmd.Context(), common.WithoutPrivateKey, -1,
func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error {
dbid, err := getSelectedDbid(cmd, conf)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/kwil-cli/cmds/database/read_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func readSchemaCmd() *cobra.Command {
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
var resp respSchema
err := common.DialClient(cmd.Context(), common.WithoutPrivateKey, func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error {
err := common.DialClient(cmd.Context(), common.WithoutPrivateKey, -1, func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error {
dbid, err := getSelectedDbid(cmd, conf)
if err != nil {
return fmt.Errorf("you must specify either a database name with the --name, or a database id with the --dbid flag")
Expand Down
2 changes: 1 addition & 1 deletion cmd/kwil-cli/cmds/utils/ping.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func pingCmd() *cobra.Command {
Long: "",
RunE: func(cmd *cobra.Command, args []string) error {
var res string
err := common.DialClient(cmd.Context(), common.WithoutPrivateKey, func(ctx context.Context, client *client.Client, cfg *config.KwilCliConfig) error {
err := common.DialClient(cmd.Context(), common.WithoutPrivateKey, -1, func(ctx context.Context, client *client.Client, cfg *config.KwilCliConfig) error {
var _err error
res, _err = client.Ping(ctx)
return _err
Expand Down
2 changes: 1 addition & 1 deletion cmd/kwil-cli/cmds/utils/tx_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func txQueryCmd() *cobra.Command {
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var resp respTxQuery
err := common.DialClient(cmd.Context(), common.WithoutPrivateKey, func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error {
err := common.DialClient(cmd.Context(), common.WithoutPrivateKey, -1, func(ctx context.Context, client *client.Client, conf *config.KwilCliConfig) error {
txHash, err := hex.DecodeString(args[0])
if err != nil {
return fmt.Errorf("error decoding transaction id: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/grpc/txsvc/v1/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func (s *Service) GetAccount(ctx context.Context, req *txpb.GetAccountRequest) (

return &txpb.GetAccountResponse{
Account: &txpb.Account{
PublicKey: acc.PublicKey,
PublicKey: acc.PublicKey, // nil for non-existent account
Nonce: acc.Nonce,
Balance: acc.Balance.String(),
},
Expand Down
2 changes: 1 addition & 1 deletion pkg/balances/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
// emptyAccount returns an empty account with a balance of 0 and a nonce of 0.
func emptyAccount() *Account {
return &Account{
PublicKey: nil,
PublicKey: nil, // do not change unless callers are updated
Balance: big.NewInt(0),
Nonce: 0,
}
Expand Down
5 changes: 3 additions & 2 deletions pkg/balances/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ func (a *AccountStore) createAccount(ctx context.Context, pubKey []byte) error {
})
}

// getAccountReadOnly gets an account using a read-only connection.
// it will not show uncommitted changes.
// getAccountReadOnly gets an account using a read-only connection. it will not
// show uncommitted changes. If the account does not exist, no error is
// returned, but an account with a nil pubkey is returned.
func (a *AccountStore) getAccountReadOnly(ctx context.Context, pubKey []byte) (*Account, error) {
results, err := a.db.Query(ctx, sqlGetAccount, map[string]interface{}{
"$public_key": pubKey,
Expand Down
6 changes: 6 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ var (
ErrNotFound = errors.New("not found")
)

// NonceFunc is used with WithNonceFunc to have external logic provide the nonce
// when making transactions.
type NonceFunc func(context.Context) (uint64, error)

// Client wraps the methods to interact with the Kwil public API.
// All the transport level details are encapsulated in the transportClient.
type Client struct {
Expand All @@ -40,6 +44,8 @@ type Client struct {
Signer auth.Signer
logger log.Logger

nonceFn NonceFunc

tlsCertFile string // the tls cert file path
}

Expand Down
14 changes: 14 additions & 0 deletions pkg/client/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ func WithSigner(signer auth.Signer) Option {
}
}

// WithNonceFunc overrides the selection of nonce when creating a transaction.
// When this is not set, the client requests the account's current highest nonce
// in a confirmed transaction. To use this, you would define a NonceFunc to
// return a nonce as required by the application. For example, it could be a
// static value for a single use client, or it could provide a new nonce each
// time it is called according to the external logic defined by NonceFunc.
// Possibly this would be better as a "TxOpts" on all of the relevant methods,
// but that's a bit more disruptive.
func WithNonceFunc(fn NonceFunc) Option {
return func(c *Client) {
c.nonceFn = fn
}
}

func WithTLSCert(certFile string) Option {
return func(c *Client) {
c.tlsCertFile = certFile
Expand Down
30 changes: 20 additions & 10 deletions pkg/client/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,36 @@ package client
import (
"context"
"fmt"
"math/big"

"github.com/kwilteam/kwil-db/pkg/balances"
"github.com/kwilteam/kwil-db/pkg/transactions"
)

// newTx creates a new Transaction signed by the Client's Signer
func (c *Client) newTx(ctx context.Context, data transactions.Payload) (*transactions.Transaction, error) {
// get nonce from address
acc, err := c.transportClient.GetAccount(ctx, c.Signer.PublicKey())
if err != nil {
acc = &balances.Account{
PublicKey: c.Signer.PublicKey(),
Nonce: 0,
Balance: big.NewInt(0),
var nonce uint64
if c.nonceFn == nil {
// Get the latest nonce for the account, if it exists.
acc, err := c.transportClient.GetAccount(ctx, c.Signer.PublicKey())
if err != nil {
return nil, err
}
// NOTE: an error type would be more robust signalling of a non-existent
// account, but presently a nil pubkey is set by pkg/balances.
if len(acc.PublicKey) > 0 {
nonce = uint64(acc.Nonce + 1)
} else {
nonce = 1
}
} else {
var err error
nonce, err = c.nonceFn(ctx)
if err != nil {
return nil, err
}
}

// build transaction
tx, err := transactions.CreateTransaction(data, uint64(acc.Nonce+1))
tx, err := transactions.CreateTransaction(data, nonce)
if err != nil {
return nil, fmt.Errorf("failed to create transaction: %w", err)
}
Expand Down

0 comments on commit da75a9e

Please sign in to comment.