client

package
v0.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 9, 2024 License: MIT Imports: 19 Imported by: 3

README

Kwil Go Client

This folder contains the Go language client for interacting with a Kwil RPC provider. Package client may be used to build a third-party application with the ability to:

  • Retrieve the status of a Kwil network.
  • List and retrieve Kuneiform schemas deployed on a Kwil network.
  • Deploy and drop schemas.
  • Execute mutative actions defined in a schema.
  • Call read-only actions without a network transaction.
  • Run ad-hoc SQL queries.
  • Retrieve account information, such as balance and nonce.
  • Check the status and execution outcome of a network transaction.

The client package is used by the kwil-cli application to provide these functions on the command line. Go applications may use the package directly.

Get the core Go Module

The client package is part of the core Go sub-module of the kwil-db repository. To use the package in your Go application, add it as a require in your project's go.mod:

$ go get github.com/kwilteam/kwil-db/core
go: downloading github.com/kwilteam/kwil-db/core v0.1.2
go: downloading github.com/kwilteam/kwil-db v0.7.2
go: added github.com/kwilteam/kwil-db/core v0.1.2

If you did not already have a go.mod for your project, create one with go mod init mykwilapp, replacing mykwilapp with the module name for your project, which is typically a remote git repository location.

Alternatively, can also manually edit your go.mod and then run go mod tidy.

Your go.mod should be similar to the following:

module mykwilapp

go 1.22

require (
    github.com/kwilteam/kwil-db/core v0.1.2
)

Import the client package

With the Kwil core module added to your go.mod, you can use the client package in your code by importing it:

import "github.com/kwilteam/kwil-db/core/client"

Using the Client type

Basic functionality

The main functionality is provided by the Client type. The NewClient function constructs a new Client instance from the URL of a Kwil RPC provider, and a set of options in the core/types/client.Options type.

For example:

package main

import (
	"context"
	"fmt"

	"github.com/kwilteam/kwil-db/core/client"
	klog "github.com/kwilteam/kwil-db/core/log"
	"github.com/kwilteam/kwil-db/core/types"
	ctypes "github.com/kwilteam/kwil-db/core/types/client"
)

const (
	provider = "https://longhorn.kwil.com"
)

func main() {
	ctx := context.Background()

	// Create the client and connect to the RPC provider.
	cl, err := client.NewClient(ctx, provider, ctypes.DefaultOptions())
	if err != nil {
		log.Fatal(err)
	}

	// Report the chain ID and block height of the provider.
	chainInfo, err := cl.ChainInfo(ctx)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Connected to Kwil chain %q, block height %d\n",
		chainInfo.ChainID, chainInfo.BlockHeight)
}
Wallet Setup

In the above example, we used ctypes.DefaultOptions(), which includes no logger, signer (wallet), or expected chain ID. To work with a Kwil account, use the crypto and crypto/auth packages to create and load private keys. For example, we can create and load a secp256k1 private key plus an Ethereum "personal" signer with the following functions:

import (
   	"github.com/kwilteam/kwil-db/core/crypto"
	"github.com/kwilteam/kwil-db/core/crypto/auth"
)

func genKey() *crypto.Secp256k1PrivateKey {
	key, _ := crypto.GenerateSecp256k1Key()
	return key // fmt.Println(key.Hex())
}

func makeSigner(keyHex string) auth.Signer {
	key, err := crypto.Secp256k1PrivateKeyFromHex(keyHex)
	if err != nil {
		panic(fmt.Sprintf("bad private key: %v", err))
	}
	return &auth.EthPersonalSigner{Key: *key}
}

Now we can expand our example application to work with our account and create signed transactions on the specified Kwil network.

const (
	chainID  = "longhorn" // expect provider to report this chain ID
	provider = "https://longhorn.kwil.com"
	privKey  = "..." // my secp256k1 private key in hexadecimal
)

func main() {
	ctx := context.Background()
	signer := makeSigner(privKey)
	acctID := signer.Identity()

	opts := &ctypes.Options{
		Logger:  klog.NewStdOut(klog.InfoLevel),
		ChainID: chainID, // ensure the provider matches
		Signer:  signer,  // required for transactions and auth
	}

	// Create the client and connect to the RPC provider.
	cl, err := client.NewClient(ctx, provider, opts)
	if err != nil {
		log.Fatal(err)
	}

	// Check our account's balance.
	acctInfo, err := cl.GetAccount(ctx, acctID, types.AccountStatusLatest)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Account %x balance = %v, nonce = %d\n", acctID, acctInfo.Balance, acctInfo.Nonce)
}
List Deployed Databases

List any existing databases with the ListDatabases method:

// List previously deployed database owned by us.
datasets, err := cl.ListDatabases(ctx, acctID)
if err != nil {
	log.Fatal(err)
}
fmt.Printf("Found %d database(s) owned by me.\n", len(datasets))

Initially there will be no owned databases. To deploy one, the account will need to be funded. If the account has no balance, use the faucet to request testnet tokens. See Manual Faucet Use to request funds for an address with no web wallet.

Databases Deployment

Now that we have a Client with a working RPC provider connection and a funded account, we can deploy and drop databases. To deploy one, use the DeployDatabase method. Unlike the methods we have used so far, this one will create, sign, and broadcast a blockchain transaction on the Kwil network. Once the transaction is included in a block and executed, the database will become available for use.

Before deploying a database, the schema definition is required. This is modeled by the core/types/transactions.Schema type. We can parse a Kuneiform .kf file using github.com/kwilteam/kuneiform/kfparser as follows:

import "github.com/kwilteam/kuneiform/kfparser"

// unmarshalKf parses the contents of a Kuneiform schema file.
func unmarshalKf(content string) (*transactions.Schema, error) {
	astSchema, err := kfparser.Parse(content)
	if err != nil {
		return nil, fmt.Errorf("failed to parse file: %w", err)
	}
	schemaJSON, err := astSchema.ToJSON()
	if err != nil {
		return nil, fmt.Errorf("failed to marshal schema: %w", err)
	}
	var db transactions.Schema
	return &db, json.Unmarshal(schemaJSON, &db)
}

In our example app, we can now use DeployDatabase:

// Use the kuneiform packages to load the schema.
schema, err := unmarshalKf(testKf)
if err != nil {
	log.Fatal(err)
}

txHash, err := cl.DeployDatabase(ctx, schema)
if err != nil {
	log.Fatal(err)
}
fmt.Printf("DeployDatabase succeeded! txHash = %x", txHash)

If that succeeded, the database deployment transaction was successfully broadcasted. The txHash is this transaction's identifier. However, the database is not yet deployed!

First we have to wait for the next block for the transaction to be executed. To ensure that the transaction is included in a block before the method returns, we may specify an option as follows:

// When broadcasting a transaction, wait until it is included in a block.
txOpts := []ctypes.TxOpt{ctypes.WithSyncBroadcast(true)}
txHash, err := cl.DeployDatabase(ctx, schema, txOpts...) // wait

In addition to broadcasting the transaction, this ensures the transaction was included in a block. The next section describes how to check the outcome of a transaction's execution.

Transaction Status

After being broadcasted to a Kwil node, a transaction must be included in a block and execute without error for the database to actually be deployed. Use the TxQuery method to check.

In our example app, we can define the following closure to use after every transaction we broadcast:

// After broadcast, we get a transaction hash that uniquely identifies the
// transaction. Use the TxQuery method to verify the execution succeeded.
checkTx := func(txHash []byte, desc string) {
	res, err := cl.TxQuery(ctx, txHash)
	if err != nil {
		log.Fatal(err)
	}
	if res.TxResult.Code == transactions.CodeOk.Uint32() {
		fmt.Printf("Success: %q in transaction %x\n", desc, txHash)
	} else {
		log.Fatalf("Fail: %q in transaction %x, Result code %d, log: %q",
			desc, txHash, res.TxResult.Code, res.TxResult.Log)
	}
}

txHash, err := cl.DeployDatabase(ctx, schema, txOpts...)
if err != nil {
	log.Fatal(err)
}
checkTx(txHash, "deploy database")

Combined the WithSyncBroadcast option in txOpts, this use of TxQuery will ensure the transaction executed without error, otherwise the application will exit.

Dropping a Database

With a successfully deployed database, use the DropDatabase method to delete a database:

txHash, err = cl.DropDatabase(ctx, dbName, txOpts...)
if err != nil {
	log.Fatal(err)
}
checkTx(txHash, "drop database")

NOTE: This is only permitted if you are the owner of the database i.e. you deployed it.

Action Execution

As with the database deploy and drop methods, action execution requires a transaction since it is used to modify data in a schema. Use the ExecuteAction method.

In the schema deployed by our example app, the action called "tag" will insert data:

const actionName = "tag"
args := [][]any{{"jon was here"}} // one execution, one argument
txHash, err := cl.ExecuteAction(ctx, dbid, actionName, args, txOpts...)
if err != nil {
	log.Fatal(err)
}
checkTx(txHash, "execute action")

In the above example, the schema's "tag" action is defined as:

action tag($msg) public {
    INSERT INTO "tags" (ident, msg) VALUES (@caller, $msg);
}

We called the "tag" action with arguments [][]any{{"jon was here"}}. This is a [][]any to support batched action execution. In this example, we execute the action once, using "jon was here" as the $msg argument.

For example, a batch of two executions of an action that requires three inputs, such as action multi_tag($msg1, $msg2, $msg3) public, might look like:

args := [][]any{
	{"first1", "first2", "first3"},    // first execution
	{"second1", "second2", "second3"}, // second execution
}

NOTE: To execute an action with no input arguments, provide nil.

View (read-only) Action Calls

To run a read-only action, which is defined with the view modifier, use the CallAction method.

For example, to call the get_all method that returns all records in the tags table:

// Use a read-only view call (no blockchain transaction) to list all entries
records, err := cl.CallAction(ctx, dbid, "get_all", nil)
if err != nil {
	log.Fatal(err)
}

The CallAction method returns the data in the core/types/client.Records type. See the godocs for this type to see the methods available for accessing the records.

Complete Example

For a complete example with the schema used in the sections above, see the code in core/client/example.

The kwil-cli CLI app is also built on the Client type, and its code can be used as a reference.

Manual Faucet Use

If you have an address with no corresponding web3 wallet to connect to the faucet web page, you can directly request funds with an HTTP POST request. For example, if the account ID of your generated key from the example app is "e52f339994377968b5ef84a04f60756ec249734d", you can use curl as follows:

$ curl -X POST --data '{"address": "0xe52f339994377968b5ef84a04f60756ec249734d"}' \
  --header 'content-type: application/json' https://kwil-faucet-server.onrender.com/funds
{"message":"Successfully sent 10 tokens to 0xe52f339994377968b5ef84a04f60756ec249734d. New balance: 14"}

Documentation

Overview

Package client defines client for interacting with the Kwil provider. It's supposed to be used as go-sdk for Kwil, currently used by the Kwil CLI.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Client

type Client struct {
	Signer auth.Signer
	// contains filtered or unexported fields
}

Client is a client that interacts with a public Kwil provider.

func NewClient

func NewClient(ctx context.Context, target string, options *clientType.Options) (c *Client, err error)

NewClient creates a Kwil client. It by default communicates with target via HTTP; chain ID of the remote host will be verified against the chain ID passed in.

func WrapClient

func WrapClient(ctx context.Context, client user.TxSvcClient, options *clientType.Options) (*Client, error)

WrapClient wraps a TxSvcClient with a Kwil client. It provides a way to use a custom rpc client with the Kwil client.

func (*Client) CallAction

func (c *Client) CallAction(ctx context.Context, dbid string, action string, inputs []any) (*clientType.Records, error)

CallAction call an action. It returns the result records.

func (*Client) ChainID

func (c *Client) ChainID() string

ChainID returns the chain ID of the remote host.

func (*Client) ChainInfo

func (c *Client) ChainInfo(ctx context.Context) (*types.ChainInfo, error)

ChainInfo get the current blockchain information like chain ID and best block height/hash.

func (*Client) DeployDatabase

func (c *Client) DeployDatabase(ctx context.Context, payload *transactions.Schema, opts ...clientType.TxOpt) (transactions.TxHash, error)

DeployDatabase deploys a database.

func (*Client) DropDatabase

func (c *Client) DropDatabase(ctx context.Context, name string, opts ...clientType.TxOpt) (transactions.TxHash, error)

DropDatabase drops a database by name, using the configured signer to derive the DB ID.

func (*Client) DropDatabaseID

func (c *Client) DropDatabaseID(ctx context.Context, dbid string, opts ...clientType.TxOpt) (transactions.TxHash, error)

DropDatabaseID drops a database by ID.

func (*Client) ExecuteAction

func (c *Client) ExecuteAction(ctx context.Context, dbid string, action string, tuples [][]any, opts ...clientType.TxOpt) (transactions.TxHash, error)

ExecuteAction executes an action. It returns the receipt, as well as outputs which is the decoded body of the receipt. It can take any number of inputs, and if multiple tuples of inputs are passed, it will execute them in the same transaction.

func (*Client) GetAccount

func (c *Client) GetAccount(ctx context.Context, acctID []byte, status types.AccountStatus) (*types.Account, error)

GetAccount gets account info by account ID. If status is AccountStatusPending, it will include the pending info.

func (*Client) GetSchema

func (c *Client) GetSchema(ctx context.Context, dbid string) (*transactions.Schema, error)

GetSchema gets a schema by dbid.

func (*Client) ListDatabases

func (c *Client) ListDatabases(ctx context.Context, owner []byte) ([]*types.DatasetIdentifier, error)

ListDatabases lists databases belonging to an owner. If no owner is passed, it will list all databases.

func (*Client) Ping

func (c *Client) Ping(ctx context.Context) (string, error)

Ping pings the remote host.

func (*Client) Query

func (c *Client) Query(ctx context.Context, dbid string, query string) (*clientType.Records, error)

Query executes a query.

func (*Client) Transfer

func (c *Client) Transfer(ctx context.Context, to []byte, amount *big.Int, opts ...clientType.TxOpt) (transactions.TxHash, error)

Transfer transfers balance to a given address.

func (*Client) TxQuery

func (c *Client) TxQuery(ctx context.Context, txHash []byte) (*transactions.TcTxQueryResponse, error)

TxQuery get transaction by hash.

func (*Client) WaitTx

func (c *Client) WaitTx(ctx context.Context, txHash []byte, interval time.Duration) (*transactions.TcTxQueryResponse, error)

WaitTx repeatedly queries at a given interval for the status of a transaction until it is confirmed (is included in a block).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL