Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 48 additions & 124 deletions pkg/lumera/Readme.md
Original file line number Diff line number Diff line change
@@ -1,153 +1,77 @@
# Lumera Client
## Lumera Client (Slim Guide)

A Go client for interacting with the Lumera blockchain.
A minimal guide to the Lumera client

## Features
What it is

- Connect to Lumera nodes via gRPC
- Interact with all Lumera modules:
- Action module - manage and query action data
- SuperNode module - interact with supernodes
- Transaction module - broadcast and query transactions
- Node module - query node status and blockchain info
- Configurable connection options
- Clean, modular API design with clear separation of interfaces and implementations
- Lightweight client over gRPC with small modules: `Auth`, `Action`, `ActionMsg`, `SuperNode`, `Tx`, `Node`.
- Shared tx pipeline for building, simulating, signing, and broadcasting messages.

## Installation

```bash
go get github.com/LumeraProtocol/lumera-client
```

## Quick Start
Create a client

```go
package main

import (
"context"
"fmt"
"log"

"github.com/LumeraProtocol/lumera-client/client"
cfg, _ := lumera.NewConfig(
"https://grpc.testnet.lumera.io", // or host:port
"chain-id",
keyName, // key name in your keyring
keyring, // cosmos-sdk keyring
)

func main() {
// Create a context
ctx := context.Background()

// Initialize the client with options
lumeraClient, err := client.NewClient(
ctx,
client.WithGRPCAddr("localhost:9090"),
client.WithChainID("lumera-mainnet"),
client.WithTimeout(30),
)
if err != nil {
log.Fatalf("Failed to create Lumera client: %v", err)
}
defer lumeraClient.Close()

// Get the latest block height
latestBlock, err := lumeraClient.Node().GetLatestBlock(ctx)
if err != nil {
log.Fatalf("Failed to get latest block: %v", err)
}

fmt.Printf("Latest block height: %d\n", latestBlock.Block.Header.Height)
}
```

## Examples

The repository includes example applications demonstrating how to use the client:

- **Basic Example**: Shows simple queries and interactions with the Lumera blockchain
- **Advanced Example**: Demonstrates a complete transaction flow with error handling and retries

To run the examples:

```bash
# Build and run the basic example
make run-basic

# Build and run the advanced example
make run-advanced
cli, _ := lumera.NewClient(ctx, cfg)
defer cli.Close()
```

## Project Structure
Using modules

```
lumera-client/
│ # Core client package
│ interface.go # Client interface definitions
│ client.go # Client implementation
│ config.go # Configuration types
│ options.go # Option functions
│ connection.go # Connection handling
├── modules/ # Module-specific packages
│ ├── action/ # Action module
│ ├── node/ # Node module
│ ├── supernode/ # SuperNode module
│ └── tx/ # Transaction module
└── examples/ # Example applications
└── main.go # Basic usage example

```
- `cli.Action()` – query actions (GetAction, GetActionFee, GetParams)
- `cli.ActionMsg()` – send action messages (see below)
- `cli.Auth()` – accounts/verify
- `cli.SuperNode()` – supernode queries
- `cli.Tx()` – tx internals (shared by helpers)
- `cli.Node()` – chain/node info

## Module Documentation
Gas and fees (tx module)

### Action Module
- Default gas price: `0.025 ulume`.
- Accepts gas price as `"0.025"` or `"0.025ulume"`.
- Validate config and surface broadcast errors automatically.

The Action module allows you to interact with Lumera actions, which are the core data processing units in the Lumera blockchain.
Override gas price at runtime (keeps other defaults):

```go
// Get action by ID
action, err := client.Action().GetAction(ctx, "action-id-123")

// Calculate fee for action with specific data size
fee, err := client.Action().GetActionFee(ctx, "1024") // 1KB data
am := cli.ActionMsg()
am.SetTxHelperConfig(&tx.TxHelperConfig{ GasPrice: "0.025ulume" })
```

### Node Module
Send actions (ActionMsg)

The Node module provides information about the blockchain and node status.
RequestAction:

```go
// Get latest block
block, err := client.Node().GetLatestBlock(ctx)

// Get specific block by height
block, err := client.Node().GetBlockByHeight(ctx, 1000)

// Get node information
nodeInfo, err := client.Node().GetNodeInfo(ctx)
resp, err := cli.ActionMsg().RequesAction(
ctx,
"CASCADE",
metadataJSON, // stringified JSON
"23800ulume", // positive integer ulume amount
fmt.Sprintf("%d", time.Now().Add(25*time.Hour).Unix()), // future expiry
)
```

### SuperNode Module

The SuperNode module allows you to interact with Lumera supernodes.
FinalizeCascadeAction:

```go
// Get top supernodes for a specific block
topNodes, err := client.SuperNode().GetTopSuperNodesForBlock(ctx, 1000)

// Get specific supernode by address
node, err := client.SuperNode().GetSuperNode(ctx, "validator-address")
resp, err := cli.ActionMsg().FinalizeCascadeAction(ctx, actionID, []string{"rqid-1", "rqid-2"})
```

### Transaction Module

The Transaction module handles transaction broadcasting and querying.
Validation rules (built-in)

```go
// Broadcast a signed transaction
resp, err := client.Tx().BroadcastTx(ctx, txBytes, sdktx.BroadcastMode_BROADCAST_MODE_SYNC)

// Simulate a transaction
sim, err := client.Tx().SimulateTx(ctx, txBytes)
- RequestAction:
- `actionType`, `metadata`, `price`, `expirationTime` required.
- `price`: must be `<positive-int>ulume`.
- `expirationTime`: future Unix seconds.
- FinalizeCascadeAction:
- `actionId` required; `rqIdsIds` must have non-empty entries.

// Get transaction by hash
tx, err := client.Tx().GetTx(ctx, "tx-hash")
```
Notes

- Method name is currently `RequesAction` (typo kept for compatibility).
- Tx uses simulation + adjustment + padding before sign/broadcast.
12 changes: 11 additions & 1 deletion pkg/lumera/codec/encoding.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package codec

import (
"sync"

actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
Expand All @@ -18,6 +20,11 @@ type EncodingConfig struct {
Amino *codec.LegacyAmino
}

var (
encOnce sync.Once
encCfg EncodingConfig
)

// NewEncodingConfig creates a new EncodingConfig with all required interfaces registered
func NewEncodingConfig() EncodingConfig {
amino := codec.NewLegacyAmino()
Expand Down Expand Up @@ -47,5 +54,8 @@ func RegisterInterfaces(registry codectypes.InterfaceRegistry) {

// GetEncodingConfig returns the standard encoding config for Lumera client
func GetEncodingConfig() EncodingConfig {
return NewEncodingConfig()
// Cache the encoding config to avoid repeated allocations and registrations
// across transactions and simulations.
encOnce.Do(func() { encCfg = NewEncodingConfig() })
return encCfg
}
69 changes: 45 additions & 24 deletions pkg/lumera/modules/action_msg/helpers.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,58 @@
package action_msg

import (
"fmt"
"fmt"
"strconv"
"time"

actionapi "github.com/LumeraProtocol/lumera/api/lumera/action"
actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types"
"google.golang.org/protobuf/encoding/protojson"
actionapi "github.com/LumeraProtocol/lumera/api/lumera/action"
actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types"
"github.com/LumeraProtocol/supernode/v2/pkg/lumera/util"
"google.golang.org/protobuf/encoding/protojson"
)

func validateRequestActionParams(actionType, metadata, price, expirationTime string) error {
if actionType == "" {
return fmt.Errorf("action type cannot be empty")
}
if metadata == "" {
return fmt.Errorf("metadata cannot be empty")
}
if price == "" {
return fmt.Errorf("price cannot be empty")
}
if expirationTime == "" {
return fmt.Errorf("expiration time cannot be empty")
}
return nil
if actionType == "" {
return fmt.Errorf("action type cannot be empty")
}
if metadata == "" {
return fmt.Errorf("metadata cannot be empty")
}
if price == "" {
return fmt.Errorf("price cannot be empty")
}
// Validate price: must be integer coin in ulume (e.g., "1000ulume")
if err := util.ValidateUlumeIntCoin(price); err != nil {
return fmt.Errorf("invalid price: %w", err)
}
if expirationTime == "" {
return fmt.Errorf("expiration time cannot be empty")
}
// Validate expiration is a future unix timestamp
exp, err := strconv.ParseInt(expirationTime, 10, 64)
if err != nil {
return fmt.Errorf("invalid expirationTime: %w", err)
}
// Allow small clock skew; require strictly in the future
if exp <= time.Now().Add(30*time.Second).Unix() {
return fmt.Errorf("expiration time must be in the future")
}
return nil
}

func validateFinalizeActionParams(actionId string, rqIdsIds []string) error {
if actionId == "" {
return fmt.Errorf("action ID cannot be empty")
}
if len(rqIdsIds) == 0 {
return fmt.Errorf("rq_ids_ids cannot be empty for cascade action")
}
return nil
if actionId == "" {
return fmt.Errorf("action ID cannot be empty")
}
if len(rqIdsIds) == 0 {
return fmt.Errorf("rq_ids_ids cannot be empty for cascade action")
}
for i, s := range rqIdsIds {
if s == "" {
return fmt.Errorf("rq_ids_ids[%d] cannot be empty", i)
}
}
return nil
}

func createRequestActionMessage(creator, actionType, metadata, price, expirationTime string) *actiontypes.MsgRequestAction {
Expand Down
3 changes: 3 additions & 0 deletions pkg/lumera/modules/action_msg/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ func (m *module) FinalizeCascadeAction(ctx context.Context, actionId string, rqI
}

func (m *module) SetTxHelperConfig(config *txmod.TxHelperConfig) {
if config == nil {
return
}
m.txHelper.UpdateConfig(config)
}

Expand Down
47 changes: 37 additions & 10 deletions pkg/lumera/modules/tx/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,43 @@ func (h *TxHelper) GetAccountInfo(ctx context.Context) (*authtypes.BaseAccount,

// UpdateConfig allows updating the transaction configuration
func (h *TxHelper) UpdateConfig(config *TxHelperConfig) {
h.config = &TxConfig{
ChainID: config.ChainID,
Keyring: config.Keyring,
KeyName: config.KeyName,
GasLimit: config.GasLimit,
GasAdjustment: config.GasAdjustment,
GasPadding: config.GasPadding,
FeeDenom: config.FeeDenom,
GasPrice: config.GasPrice,
}
// Merge provided fields with existing config to avoid zeroing defaults
if h.config == nil {
h.config = &TxConfig{}
}

// ChainID
if config.ChainID != "" {
h.config.ChainID = config.ChainID
}
// Keyring
if config.Keyring != nil {
h.config.Keyring = config.Keyring
}
// KeyName
if config.KeyName != "" {
h.config.KeyName = config.KeyName
}
// GasLimit
if config.GasLimit != 0 {
h.config.GasLimit = config.GasLimit
}
// GasAdjustment
if config.GasAdjustment != 0 {
h.config.GasAdjustment = config.GasAdjustment
}
// GasPadding
if config.GasPadding != 0 {
h.config.GasPadding = config.GasPadding
}
// FeeDenom
if config.FeeDenom != "" {
h.config.FeeDenom = config.FeeDenom
}
// GasPrice
if config.GasPrice != "" {
h.config.GasPrice = config.GasPrice
}
}

// GetConfig returns the current transaction configuration
Expand Down
Loading