Skip to content

Commit 61605c5

Browse files
authored
feat: use custom HTTP clients (#163)
1 parent 5730554 commit 61605c5

File tree

9 files changed

+704
-33
lines changed

9 files changed

+704
-33
lines changed

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,46 @@ If you need help, don't hesitate to [open an issue](https://github.com/trufnetwo
2020
go get github.com/trufnetwork/sdk-go
2121
```
2222

23+
### Advanced Configuration
24+
25+
#### Custom Transport (Optional)
26+
27+
The SDK supports custom transport implementations for specialized environments. By default, the SDK uses `HTTPTransport` with standard `net/http`, but you can provide alternative transports:
28+
29+
```go
30+
// Default usage (HTTPTransport automatically created)
31+
client, err := tnclient.NewClient(ctx, endpoint,
32+
tnclient.WithSigner(signer),
33+
)
34+
35+
// Advanced: Custom transport for specialized environments
36+
// (e.g., Chainlink Runtime Environment)
37+
customTransport := &MyCustomTransport{...}
38+
client, err := tnclient.NewClient(ctx, endpoint,
39+
tnclient.WithSigner(signer),
40+
tnclient.WithTransport(customTransport),
41+
)
42+
```
43+
44+
**When to use custom transports:**
45+
- Chainlink Runtime Environment (CRE) workflows
46+
- Testing with mock transports
47+
- Custom HTTP client requirements
48+
- Alternative RPC protocols
49+
50+
**Advanced API Access:**
51+
52+
For advanced use cases requiring direct `GatewayClient` access (HTTP transport only):
53+
54+
```go
55+
if gwClient := client.GetKwilClient(); gwClient != nil {
56+
// Direct low-level access for advanced scenarios
57+
result, err := gwClient.Call(ctx, "", "custom_action", args)
58+
}
59+
```
60+
61+
> **Note**: For most use cases, prefer the high-level Client methods (`ListStreams`, `DeployStream`, etc.) which are transport-agnostic and work with any transport implementation.
62+
2363
## Local Node Testing
2464

2565
### Setting Up a Local Node

core/tnclient/client.go

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66

77
"github.com/go-playground/validator/v10"
88
"github.com/pkg/errors"
9-
kwilClientType "github.com/trufnetwork/kwil-db/core/client/types"
109
"github.com/trufnetwork/kwil-db/core/crypto/auth"
1110
"github.com/trufnetwork/kwil-db/core/gatewayclient"
1211
"github.com/trufnetwork/kwil-db/core/log"
@@ -20,10 +19,9 @@ import (
2019
)
2120

2221
type Client struct {
23-
Signer auth.Signer `validate:"required"`
24-
logger *log.Logger
25-
kwilClient *gatewayclient.GatewayClient `validate:"required"`
26-
kwilOptions *gatewayclient.GatewayOptions
22+
signer auth.Signer `validate:"required"`
23+
logger *log.Logger
24+
transport Transport `validate:"required"`
2725
}
2826

2927
var _ clientType.Client = (*Client)(nil)
@@ -32,21 +30,28 @@ type Option func(*Client)
3230

3331
func NewClient(ctx context.Context, provider string, options ...Option) (*Client, error) {
3432
c := &Client{}
35-
c.kwilOptions = &gatewayclient.GatewayOptions{
36-
Options: *kwilClientType.DefaultOptions(),
37-
}
33+
34+
// Apply user-provided options
3835
for _, option := range options {
3936
option(c)
4037
}
4138

42-
kwilClient, err := gatewayclient.NewClient(ctx, provider, c.kwilOptions)
43-
if err != nil {
44-
return nil, errors.WithStack(err)
39+
// Create default HTTPTransport if no transport was provided via options
40+
if c.transport == nil {
41+
var logger log.Logger
42+
if c.logger != nil {
43+
logger = *c.logger
44+
}
45+
46+
transport, err := NewHTTPTransport(ctx, provider, c.signer, logger)
47+
if err != nil {
48+
return nil, errors.Wrap(err, "failed to create default HTTP transport")
49+
}
50+
c.transport = transport
4551
}
46-
c.kwilClient = kwilClient
4752

4853
// Validate the client
49-
if err = c.Validate(); err != nil {
54+
if err := c.Validate(); err != nil {
5055
return nil, errors.WithStack(err)
5156
}
5257

@@ -60,28 +65,64 @@ func (c *Client) Validate() error {
6065

6166
func WithSigner(signer auth.Signer) Option {
6267
return func(c *Client) {
63-
c.kwilOptions.Signer = signer
64-
c.Signer = signer
68+
c.signer = signer
6569
}
6670
}
6771

6872
func WithLogger(logger log.Logger) Option {
6973
return func(c *Client) {
7074
c.logger = &logger
71-
c.kwilOptions.Logger = logger
75+
}
76+
}
77+
78+
// WithTransport configures the client to use a custom transport implementation.
79+
//
80+
// By default, the SDK uses HTTPTransport which communicates via standard net/http.
81+
// This option allows you to substitute a different transport (e.g., for Chainlink CRE,
82+
// mock testing, or custom protocols).
83+
//
84+
// Example:
85+
//
86+
// transport, _ := NewHTTPTransport(ctx, endpoint, signer, logger)
87+
// client, err := NewClient(ctx, endpoint,
88+
// tnclient.WithTransport(transport),
89+
// )
90+
//
91+
// Note: When using WithTransport, the provider URL passed to NewClient is ignored
92+
// since the transport is already configured.
93+
func WithTransport(transport Transport) Option {
94+
return func(c *Client) {
95+
c.transport = transport
7296
}
7397
}
7498

7599
func (c *Client) GetSigner() auth.Signer {
76-
return c.kwilClient.Signer()
100+
return c.transport.Signer()
77101
}
78102

79103
func (c *Client) WaitForTx(ctx context.Context, txHash kwilType.Hash, interval time.Duration) (*kwilType.TxQueryResponse, error) {
80-
return c.kwilClient.WaitTx(ctx, txHash, interval)
104+
return c.transport.WaitTx(ctx, txHash, interval)
81105
}
82106

107+
// GetKwilClient returns the underlying GatewayClient if using HTTPTransport.
108+
//
109+
// This method provides direct access to the GatewayClient for advanced use cases
110+
// that require low-level control. For most scenarios, prefer using the Client's
111+
// high-level methods (ListStreams, DeployStream, etc.) which are transport-agnostic.
112+
//
113+
// Returns nil if using a non-HTTP transport (e.g., CRE transport).
114+
//
115+
// Example:
116+
//
117+
// if gwClient := client.GetKwilClient(); gwClient != nil {
118+
// // Direct GatewayClient access for advanced use cases
119+
// result, err := gwClient.Call(ctx, "", "custom_action", args)
120+
// }
83121
func (c *Client) GetKwilClient() *gatewayclient.GatewayClient {
84-
return c.kwilClient
122+
if httpTransport, ok := c.transport.(*HTTPTransport); ok {
123+
return httpTransport.gatewayClient
124+
}
125+
return nil
85126
}
86127

87128
func (c *Client) DeployStream(ctx context.Context, streamId util.StreamId, streamType clientType.StreamType) (types.Hash, error) {
@@ -101,31 +142,31 @@ func (c *Client) DestroyStream(ctx context.Context, streamId util.StreamId) (typ
101142

102143
func (c *Client) LoadActions() (clientType.IAction, error) {
103144
return tn_api.LoadAction(tn_api.NewActionOptions{
104-
Client: c.kwilClient,
145+
Client: c.GetKwilClient(),
105146
})
106147
}
107148

108149
func (c *Client) LoadPrimitiveActions() (clientType.IPrimitiveAction, error) {
109150
return tn_api.LoadPrimitiveActions(tn_api.NewActionOptions{
110-
Client: c.kwilClient,
151+
Client: c.GetKwilClient(),
111152
})
112153
}
113154

114155
func (c *Client) LoadComposedActions() (clientType.IComposedAction, error) {
115156
return tn_api.LoadComposedActions(tn_api.NewActionOptions{
116-
Client: c.kwilClient,
157+
Client: c.GetKwilClient(),
117158
})
118159
}
119160

120161
func (c *Client) LoadRoleManagementActions() (clientType.IRoleManagement, error) {
121162
return tn_api.LoadRoleManagementActions(tn_api.NewRoleManagementOptions{
122-
Client: c.kwilClient,
163+
Client: c.GetKwilClient(),
123164
})
124165
}
125166

126167
func (c *Client) LoadAttestationActions() (clientType.IAttestationAction, error) {
127168
return tn_api.LoadAttestationActions(tn_api.AttestationActionOptions{
128-
Client: c.kwilClient,
169+
Client: c.GetKwilClient(),
129170
})
130171
}
131172

@@ -140,7 +181,7 @@ func (c *Client) LoadAttestationActions() (clientType.IAttestationAction, error)
140181
// txEvent, err := txActions.GetTransactionEvent(ctx, ...)
141182
func (c *Client) LoadTransactionActions() (clientType.ITransactionAction, error) {
142183
return tn_api.LoadTransactionActions(tn_api.TransactionActionOptions{
143-
Client: c.kwilClient,
184+
Client: c.GetKwilClient(),
144185
})
145186
}
146187

@@ -152,7 +193,7 @@ func (c *Client) OwnStreamLocator(streamId util.StreamId) clientType.StreamLocat
152193
}
153194

154195
func (c *Client) Address() util.EthereumAddress {
155-
addr, err := auth.EthSecp256k1Authenticator{}.Identifier(c.kwilClient.Signer().CompactID())
196+
addr, err := auth.EthSecp256k1Authenticator{}.Identifier(c.transport.Signer().CompactID())
156197
if err != nil {
157198
// should never happen
158199
logging.Logger.Panic("failed to get address from signer", zap.Error(err))

core/tnclient/list_streams.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func (c *Client) ListStreams(ctx context.Context, input types.ListStreamsInput)
1818
args = append(args, input.OrderBy)
1919
args = append(args, input.BlockHeight)
2020

21-
result, err := c.kwilClient.Call(ctx, "", "list_streams", args)
21+
result, err := c.transport.Call(ctx, "", "list_streams", args)
2222
if err != nil || result.Error != nil {
2323
if err != nil {
2424
return nil, errors.WithStack(err)

core/tnclient/transport.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package tnclient
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
clientType "github.com/trufnetwork/kwil-db/core/client/types"
8+
"github.com/trufnetwork/kwil-db/core/crypto/auth"
9+
"github.com/trufnetwork/kwil-db/core/types"
10+
)
11+
12+
// Transport abstracts the communication layer for TRUF Network operations.
13+
// This interface allows using different transport implementations without changing SDK code.
14+
//
15+
// The default implementation (HTTPTransport) uses standard net/http via kwil-db's
16+
// GatewayClient. Custom implementations can use different protocols such as:
17+
// - Chainlink CRE's HTTP client (for workflows in CRE environments)
18+
// - gRPC or other RPC protocols
19+
// - Mock implementations for testing
20+
//
21+
// Example custom transport usage:
22+
//
23+
// type MyTransport struct { ... }
24+
//
25+
// func (t *MyTransport) Call(ctx context.Context, namespace string, action string, inputs []any) (*types.CallResult, error) {
26+
// // Custom implementation
27+
// return &types.CallResult{...}, nil
28+
// }
29+
//
30+
// // Use custom transport
31+
// client, err := tnclient.NewClient(ctx, endpoint,
32+
// tnclient.WithSigner(signer),
33+
// tnclient.WithTransport(myTransport),
34+
// )
35+
//
36+
// All SDK methods internally use the Transport interface, making the entire SDK
37+
// adaptable to different execution environments without code changes.
38+
type Transport interface {
39+
// Call executes a read-only action and returns results.
40+
// Namespace is typically "" for global actions.
41+
//
42+
// Parameters:
43+
// - ctx: Context for cancellation and timeouts
44+
// - namespace: Schema namespace (typically "" for global procedures)
45+
// - action: The procedure/action name to call
46+
// - inputs: Action input parameters as a slice of any
47+
//
48+
// Returns:
49+
// - CallResult containing the query results
50+
// - Error if the call fails or returns an application error
51+
Call(ctx context.Context, namespace string, action string, inputs []any) (*types.CallResult, error)
52+
53+
// Execute performs a write action and returns the transaction hash.
54+
// Inputs is a slice of argument arrays for batch operations.
55+
// Options can include nonce, fee, and other transaction parameters.
56+
//
57+
// Parameters:
58+
// - ctx: Context for cancellation and timeouts
59+
// - namespace: Schema namespace (typically "" for global procedures)
60+
// - action: The procedure/action name to execute
61+
// - inputs: Batch of input parameter arrays ([][]any for multiple calls)
62+
// - opts: Optional transaction options (nonce, fee, etc.)
63+
//
64+
// Returns:
65+
// - Transaction hash for tracking the transaction
66+
// - Error if the execution fails
67+
Execute(ctx context.Context, namespace string, action string, inputs [][]any, opts ...clientType.TxOpt) (types.Hash, error)
68+
69+
// WaitTx polls for transaction confirmation with the specified interval.
70+
// Blocks until the transaction is confirmed or the context is cancelled.
71+
//
72+
// Parameters:
73+
// - ctx: Context for cancellation and timeouts
74+
// - txHash: The transaction hash to wait for
75+
// - interval: Polling interval between status checks
76+
//
77+
// Returns:
78+
// - TxQueryResponse containing the transaction status and result
79+
// - Error if the wait fails or transaction is rejected
80+
WaitTx(ctx context.Context, txHash types.Hash, interval time.Duration) (*types.TxQueryResponse, error)
81+
82+
// ChainID returns the network chain identifier.
83+
// This is used to ensure transactions are sent to the correct network.
84+
//
85+
// Returns:
86+
// - Chain ID string (e.g., "truf-mainnet")
87+
ChainID() string
88+
89+
// Signer returns the cryptographic signer used for transaction authentication.
90+
// Returns nil if no signer is configured (read-only mode).
91+
//
92+
// Returns:
93+
// - Signer instance for transaction signing
94+
Signer() auth.Signer
95+
}

0 commit comments

Comments
 (0)