Skip to content

Commit 32d0f03

Browse files
authored
Add ICA controller support and refactor blockchain client base (#9)
Introduced a full ICA controller implementation in the new ica/ package (registration, packet building, ack parsing) and moved the old cascade ICA helpers/tests into this package. Added a shared blockchain/base package (config, gRPC client, tx helpers) and refactor the Lumera blockchain client to use it, including default HRP/fee denom/gas price handling. Removed the package-level cascade.CreateRequestActionMessage, update request tests to use the client method, and adjust cascade logging behavior. Expanded types to include Action.AppPubkey. Updated examples and docs (API + developer guide) to reflect ICA flow changes and new cascade request construction. Updated config/logging to use zap with configurable log levels; remove the old pkg/log logger; refresh deps in go.sum; minor Makefile/README tweaks.
1 parent 049dfb7 commit 32d0f03

40 files changed

Lines changed: 1207 additions & 383 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ help: ## Display this help screen
1919

2020
# SDK/library compile check (Go libs are compiled as part of go build; no artifact)
2121
sdk: go.sum ## Compile all packages in the module (verifies SDK builds)
22-
@echo "Compiling SDK (library packages)..."
22+
@echo "Building Lumera SDK-go (library packages)..."
2323
@$(GO) build ./...
2424
@echo "SDK build completed successfully."
2525

README.md

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func main() {
5656
RPCEndpoint: "http://localhost:26657",
5757
Address: "lumera1abc...",
5858
KeyName: "my-key",
59-
}, kr, lumerasdk.WithLogger(log.Default()))
59+
}, kr, lumerasdk.WithLogger(zap.NewExample()))
6060
if err != nil {
6161
log.Fatal(err)
6262
}
@@ -133,7 +133,7 @@ Notes:
133133

134134
#### Sending ICA RequestAction (helpers + CLI)
135135

136-
The SDK includes small helpers to assemble ICS-27 packets and decode acknowledgements:
136+
The SDK includes small helpers to assemble ICS-27 packets and decode acknowledgements in `ica`:
137137

138138
- `PackRequestForICA`: packs `MsgRequestAction` into `google.protobuf.Any` bytes.
139139
- `BuildICAPacketData`: wraps one or more Any messages into `InterchainAccountPacketData` for `EXECUTE_TX`.
@@ -149,11 +149,11 @@ msg, _, _ := cascadeClient.CreateRequestActionMessage(ctx, icaAddr, filePath, &c
149149
AppPubkey: simdPubkey,
150150
})
151151

152-
anyBz, _ := cascade.PackRequestForICA(msg)
152+
anyBz, _ := ica.PackRequestForICA(msg)
153153
var any codectypes.Any
154154
_ = gogoproto.Unmarshal(anyBz, &any)
155155

156-
packet, _ := cascade.BuildICAPacketData([]*codectypes.Any{&any})
156+
packet, _ := ica.BuildICAPacketData([]*codectypes.Any{&any})
157157
packetJSON, _ := codec.NewProtoCodec(codectypes.NewInterfaceRegistry()).MarshalJSON(&packet)
158158
_ = os.WriteFile("ica-packet.json", packetJSON, 0o600)
159159
```
@@ -174,13 +174,13 @@ simd tx interchain-accounts controller send-tx <connection-id> ica-packet.json \
174174
Once you have the IBC acknowledgement bytes (from relayer output or packet-ack query), decode action IDs:
175175

176176
```go
177-
ids, err := cascade.ExtractRequestActionIDsFromAck(ackBytes)
177+
ids, err := ica.ExtractRequestActionIDsFromAck(ackBytes)
178178
```
179179

180180
If you already have `sdk.TxMsgData` (for example, from an ack you decoded yourself), use:
181181

182182
```go
183-
ids := cascade.ExtractRequestActionIDsFromTxMsgData(msgData)
183+
ids := ica.ExtractRequestActionIDsFromTxMsgData(msgData)
184184
```
185185

186186
Packet/ack CLI helpers (controller chain):
@@ -193,10 +193,10 @@ simd q ibc channel packet-ack <port> <channel> <sequence> --output json
193193
```
194194

195195
```go
196-
txHash, _ := cascade.ParseTxHashJSON(txJSON)
197-
packetInfo, _ := cascade.ExtractPacketInfoFromTxJSON(txQueryJSON)
198-
ackBytes, _ := cascade.DecodePacketAcknowledgementJSON(ackQueryJSON)
199-
ids, _ := cascade.ExtractRequestActionIDsFromAck(ackBytes)
196+
txHash, _ := ica.ParseTxHashJSON(txJSON)
197+
packetInfo, _ := ica.ExtractPacketInfoFromTxJSON(txQueryJSON)
198+
ackBytes, _ := ica.DecodePacketAcknowledgementJSON(ackQueryJSON)
199+
ids, _ := ica.ExtractRequestActionIDsFromAck(ackBytes)
200200
_ = txHash
201201
_ = packetInfo
202202
```
@@ -211,10 +211,13 @@ Common helpers:
211211
- `NewDefaultTxConfig` and `SignTxWithKeyring` for signing with Cosmos SDK builders.
212212

213213
```go
214-
import sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto"
214+
import (
215+
"github.com/LumeraProtocol/sdk-go/constants"
216+
sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto"
217+
)
215218

216219
kr, _ := sdkcrypto.NewKeyring(sdkcrypto.DefaultKeyringParams())
217-
addr, _ := sdkcrypto.AddressFromKey(kr, "alice", "lumera")
220+
addr, _ := sdkcrypto.AddressFromKey(kr, "alice", constants.LumeraAccountHRP)
218221
_ = addr
219222
```
220223

@@ -228,6 +231,7 @@ import (
228231

229232
"github.com/cosmos/cosmos-sdk/crypto/keyring"
230233
lumerasdk "github.com/LumeraProtocol/sdk-go/client"
234+
"github.com/LumeraProtocol/sdk-go/constants"
231235
sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto"
232236
)
233237

@@ -238,8 +242,8 @@ factory, err := lumerasdk.NewFactory(lumerasdk.Config{
238242
RPCEndpoint: "http://localhost:26657",
239243
}, kr)
240244

241-
aliceAddr, _ := sdkcrypto.AddressFromKey(kr, "alice", "lumera")
242-
bobAddr, _ := sdkcrypto.AddressFromKey(kr, "bob", "lumera")
245+
aliceAddr, _ := sdkcrypto.AddressFromKey(kr, "alice", constants.LumeraAccountHRP)
246+
bobAddr, _ := sdkcrypto.AddressFromKey(kr, "bob", constants.LumeraAccountHRP)
243247

244248
alice, _ := factory.WithSigner(ctx, aliceAddr, "alice")
245249
bob, _ := factory.WithSigner(ctx, bobAddr, "bob")

blockchain/base/client.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package base
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/cosmos/cosmos-sdk/crypto/keyring"
9+
"google.golang.org/grpc"
10+
"google.golang.org/grpc/credentials"
11+
"google.golang.org/grpc/credentials/insecure"
12+
13+
clientconfig "github.com/LumeraProtocol/sdk-go/client/config"
14+
)
15+
16+
// Client provides common Cosmos SDK gRPC and tx helpers.
17+
type Client struct {
18+
conn *grpc.ClientConn
19+
config Config
20+
keyring keyring.Keyring
21+
keyName string
22+
}
23+
24+
// New creates a base blockchain client with a gRPC connection.
25+
func New(ctx context.Context, cfg Config, kr keyring.Keyring, keyName string) (*Client, error) {
26+
// Determine if we should use TLS based on the endpoint.
27+
// Use TLS if: port is 443, or hostname doesn't start with "localhost"/"127.0.0.1".
28+
useTLS := shouldUseTLS(cfg.GRPCAddr)
29+
if cfg.InsecureGRPC {
30+
useTLS = false
31+
}
32+
33+
var creds credentials.TransportCredentials
34+
if useTLS {
35+
// Use system TLS credentials for secure connections
36+
creds = credentials.NewTLS(nil)
37+
} else {
38+
// Use insecure credentials for local development
39+
creds = insecure.NewCredentials()
40+
}
41+
42+
// Create gRPC connection
43+
dialOpts := []grpc.DialOption{
44+
grpc.WithTransportCredentials(creds),
45+
grpc.WithDefaultCallOptions(
46+
grpc.MaxCallRecvMsgSize(cfg.MaxRecvMsgSize),
47+
grpc.MaxCallSendMsgSize(cfg.MaxSendMsgSize),
48+
),
49+
}
50+
51+
clientconfig.ApplyWaitTxDefaults(&cfg.WaitTx)
52+
53+
conn, err := grpc.NewClient(cfg.GRPCAddr, dialOpts...)
54+
if err != nil {
55+
return nil, fmt.Errorf("failed to connect to gRPC: %w", err)
56+
}
57+
58+
return &Client{
59+
conn: conn,
60+
config: cfg,
61+
keyring: kr,
62+
keyName: keyName,
63+
}, nil
64+
}
65+
66+
// Close closes the underlying gRPC connection.
67+
func (c *Client) Close() error {
68+
if c.conn != nil {
69+
return c.conn.Close()
70+
}
71+
return nil
72+
}
73+
74+
// GRPCConn exposes the underlying gRPC connection for specialized queries.
75+
func (c *Client) GRPCConn() *grpc.ClientConn {
76+
return c.conn
77+
}
78+
79+
// shouldUseTLS determines if TLS should be used based on the gRPC address.
80+
func shouldUseTLS(addr string) bool {
81+
// Check for explicit port 443 (standard HTTPS/gRPC-TLS port).
82+
if strings.HasSuffix(addr, ":443") {
83+
return true
84+
}
85+
86+
// Check if it's a local address (localhost, 127.0.0.1, or no hostname).
87+
if strings.HasPrefix(addr, "localhost:") ||
88+
strings.HasPrefix(addr, "127.0.0.1:") ||
89+
strings.HasPrefix(addr, "0.0.0.0:") ||
90+
strings.HasPrefix(addr, ":") { // Just port, implies localhost.
91+
return false
92+
}
93+
94+
// For any other remote address, prefer TLS by default for security.
95+
if !strings.Contains(addr, "localhost") &&
96+
!strings.Contains(addr, "127.0.0.1") &&
97+
!strings.Contains(addr, "0.0.0.0") {
98+
return true
99+
}
100+
101+
return false
102+
}

blockchain/base/config.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package base
2+
3+
import (
4+
"time"
5+
6+
sdkmath "cosmossdk.io/math"
7+
8+
clientconfig "github.com/LumeraProtocol/sdk-go/client/config"
9+
)
10+
11+
// Config captures shared Cosmos SDK chain settings for gRPC + tx workflows.
12+
type Config struct {
13+
ChainID string
14+
GRPCAddr string
15+
RPCEndpoint string
16+
AccountHRP string
17+
FeeDenom string
18+
GasPrice sdkmath.LegacyDec
19+
Timeout time.Duration
20+
MaxRecvMsgSize int
21+
MaxSendMsgSize int
22+
InsecureGRPC bool
23+
WaitTx clientconfig.WaitTxConfig
24+
}
Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
package blockchain
1+
package base
22

33
import (
44
"context"
55
"fmt"
6-
"math"
6+
"strings"
77
"time"
88

99
txtypes "cosmossdk.io/api/cosmos/tx/v1beta1"
@@ -13,11 +13,11 @@ import (
1313
"google.golang.org/grpc/codes"
1414
"google.golang.org/grpc/status"
1515

16-
sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto"
1716
waittx "github.com/LumeraProtocol/sdk-go/internal/wait-tx"
17+
sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto"
1818
)
1919

20-
// Simulate runs a gas simulation for a provided tx bytes
20+
// Simulate runs a gas simulation for a provided tx bytes.
2121
func (c *Client) Simulate(ctx context.Context, txBytes []byte) (uint64, error) {
2222
svc := txtypes.NewServiceClient(c.conn)
2323
resp, err := svc.Simulate(ctx, &txtypes.SimulateRequest{
@@ -32,7 +32,7 @@ func (c *Client) Simulate(ctx context.Context, txBytes []byte) (uint64, error) {
3232
return resp.GasInfo.GasUsed, nil
3333
}
3434

35-
// Broadcast broadcasts a signed transaction with a chosen broadcast mode
35+
// Broadcast broadcasts a signed transaction with a chosen broadcast mode.
3636
func (c *Client) Broadcast(ctx context.Context, txBytes []byte, mode txtypes.BroadcastMode) (string, error) {
3737
svc := txtypes.NewServiceClient(c.conn)
3838
resp, err := svc.BroadcastTx(ctx, &txtypes.BroadcastTxRequest{
@@ -69,6 +69,22 @@ func (c *Client) BuildAndSignTxWithGasAdjustment(ctx context.Context, msg sdk.Ms
6969
}
7070

7171
func (c *Client) buildAndSignTx(ctx context.Context, msg sdk.Msg, memo string, gasAdjustment float64) ([]byte, error) {
72+
if c.keyring == nil {
73+
return nil, fmt.Errorf("keyring is required")
74+
}
75+
if strings.TrimSpace(c.keyName) == "" {
76+
return nil, fmt.Errorf("key name is required")
77+
}
78+
if strings.TrimSpace(c.config.AccountHRP) == "" {
79+
return nil, fmt.Errorf("account HRP is required")
80+
}
81+
if strings.TrimSpace(c.config.FeeDenom) == "" {
82+
return nil, fmt.Errorf("fee denom is required")
83+
}
84+
if c.config.GasPrice.IsNil() || c.config.GasPrice.IsZero() {
85+
return nil, fmt.Errorf("gas price is required")
86+
}
87+
7288
// 1) Tx config and builder
7389
txCfg := sdkcrypto.NewDefaultTxConfig()
7490
builder := txCfg.NewTxBuilder()
@@ -84,14 +100,14 @@ func (c *Client) buildAndSignTx(ctx context.Context, msg sdk.Msg, memo string, g
84100
if err != nil {
85101
return nil, fmt.Errorf("load key %q: %w", c.keyName, err)
86102
}
87-
accAddr, err := rec.GetAddress()
103+
accAddr, err := sdkcrypto.AddressFromKey(c.keyring, c.keyName, c.config.AccountHRP)
88104
if err != nil {
89-
return nil, fmt.Errorf("get address for %q: %w", c.keyName, err)
105+
return nil, fmt.Errorf("derive address for %q: %w", c.keyName, err)
90106
}
91107

92108
authq := authtypes.NewQueryClient(c.conn)
93109
acctResp, err := authq.AccountInfo(ctx, &authtypes.QueryAccountInfoRequest{
94-
Address: accAddr.String(),
110+
Address: accAddr,
95111
})
96112
if err != nil {
97113
return nil, fmt.Errorf("query account info: %w", err)
@@ -145,8 +161,8 @@ func (c *Client) buildAndSignTx(ctx context.Context, msg sdk.Msg, memo string, g
145161
}
146162

147163
// Ensure a minimum fee to satisfy chain requirements
148-
fee := int64(math.Ceil(float64(gas) / 40.0)) //the gas price is 0.025
149-
minFee := sdk.NewCoins(sdk.NewInt64Coin("ulume", fee))
164+
feeDec := c.config.GasPrice.MulInt64(int64(gas)).Ceil().TruncateInt()
165+
minFee := sdk.NewCoins(sdk.NewCoin(c.config.FeeDenom, feeDec))
150166
builder.SetFeeAmount(minFee)
151167

152168
// 5) Sign with real credentials, overwriting placeholder
@@ -179,6 +195,43 @@ func (c *Client) GetTx(ctx context.Context, hash string) (*txtypes.GetTxResponse
179195
return resp, nil
180196
}
181197

198+
// GetTxsByEvents searches for transactions matching event filters.
199+
func (c *Client) GetTxsByEvents(ctx context.Context, events []string, page, limit uint64) (*txtypes.GetTxsEventResponse, error) {
200+
if len(events) == 0 {
201+
return nil, fmt.Errorf("events are required")
202+
}
203+
filtered := make([]string, 0, len(events))
204+
for _, evt := range events {
205+
evt = strings.TrimSpace(evt)
206+
if evt != "" {
207+
filtered = append(filtered, evt)
208+
}
209+
}
210+
if len(filtered) == 0 {
211+
return nil, fmt.Errorf("events are required")
212+
}
213+
if page == 0 {
214+
page = 1
215+
}
216+
query := strings.Join(filtered, " AND ")
217+
req := &txtypes.GetTxsEventRequest{
218+
Events: append([]string(nil), filtered...),
219+
Query: query,
220+
OrderBy: txtypes.OrderBy_ORDER_BY_DESC,
221+
}
222+
req.Page = page
223+
req.Limit = limit
224+
svc := txtypes.NewServiceClient(c.conn)
225+
resp, err := svc.GetTxsEvent(ctx, req)
226+
if err != nil {
227+
return nil, fmt.Errorf("get txs by events: %w", err)
228+
}
229+
if resp == nil {
230+
return nil, fmt.Errorf("empty get txs by events response")
231+
}
232+
return resp, nil
233+
}
234+
182235
// WaitForTxInclusion waits for a transaction to reach a final state using a
183236
// websocket subscriber when possible and falling back to periodic gRPC polling.
184237
// A new waiter (and therefore a new websocket subscription) is created for each
@@ -260,7 +313,7 @@ func (c *Client) ExtractEventAttribute(tx *txtypes.GetTxResponse, eventType, att
260313
if ev == nil {
261314
continue
262315
}
263-
// Note: abci.Event uses GetType_() since 'type' is a reserved field name
316+
// Note: abci.Event uses GetType_() since 'type' is a reserved field name.
264317
if ev.GetType_() == eventType {
265318
for _, attr := range ev.GetAttributes() {
266319
if attr == nil {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package blockchain
1+
package base
22

33
import (
44
"context"

0 commit comments

Comments
 (0)