From 56cda6c0561dcaac006eb75e19f558b4df92371c Mon Sep 17 00:00:00 2001 From: Quint Daenen Date: Wed, 5 Feb 2025 18:14:28 +0100 Subject: [PATCH] Close http connections. --- agent.go | 23 +---- client.go | 84 ++++++++++----- clients/registry/proto/v1/operator.pb.go | 56 +++++----- gen/generator.go | 4 + gen/generator_test.go | 124 +++++++++++++++++++++++ gen/templates/agent.gotmpl | 4 +- gen/templates/agent_indirect.gotmpl | 4 +- status_test.go | 2 +- 8 files changed, 221 insertions(+), 80 deletions(-) create mode 100644 gen/generator_test.go diff --git a/agent.go b/agent.go index 8f16a95..0387c58 100644 --- a/agent.go +++ b/agent.go @@ -7,7 +7,6 @@ import ( "encoding/hex" "errors" "fmt" - "net/url" "reflect" "time" @@ -23,12 +22,6 @@ import ( // DefaultConfig is the default configuration for an Agent. var DefaultConfig = Config{} -// ic0 is the old (default) host for the Internet Computer. -// var ic0, _ = url.Parse("https://ic0.app/") - -// icp0 is the default host for the Internet Computer. -var icp0, _ = url.Parse("https://icp0.io/") - func effectiveCanisterID(canisterID principal.Principal, args []any) principal.Principal { // If the canisterID is not aaaaa-aa (encoded as empty byte array), return it. if 0 < len(canisterID.Raw) || len(args) < 1 { @@ -171,17 +164,7 @@ func New(cfg Config) (*Agent, error) { if cfg.Identity != nil { id = cfg.Identity } - var logger Logger = new(NoopLogger) - if cfg.Logger != nil { - logger = cfg.Logger - } - ccfg := ClientConfig{ - Host: icp0, - } - if cfg.ClientConfig != nil { - ccfg = *cfg.ClientConfig - } - client := NewClientWithLogger(ccfg, logger) + client := NewClient(cfg.ClientConfig...) rootKey, _ := hex.DecodeString(certification.RootKey) if cfg.FetchRootKey { status, err := client.Status() @@ -204,7 +187,7 @@ func New(cfg Config) (*Agent, error) { identity: id, ingressExpiry: cfg.IngressExpiry, rootKey: rootKey, - logger: logger, + logger: client.logger, delay: delay, timeout: timeout, verifySignatures: !cfg.DisableSignedQueryVerification, @@ -501,7 +484,7 @@ type Config struct { // The default is set to 5 minutes. IngressExpiry time.Duration // ClientConfig is the configuration for the underlying Client. - ClientConfig *ClientConfig + ClientConfig []ClientOption // FetchRootKey determines whether the root key should be fetched from the IC. FetchRootKey bool // Logger is the logger used by the Agent. diff --git a/client.go b/client.go index ffedc4b..8002fa7 100644 --- a/client.go +++ b/client.go @@ -13,32 +13,30 @@ import ( "github.com/fxamacker/cbor/v2" ) +// ic0 is the old (default) host for the Internet Computer. +// var ic0, _ = url.Parse("https://ic0.app/") + +// icp0 is the default host for the Internet Computer. +var icp0, _ = url.Parse("https://icp0.io/") + // Client is a client for the IC agent. type Client struct { - client http.Client - config ClientConfig + client *http.Client + host *url.URL logger Logger } // NewClient creates a new client based on the given configuration. -func NewClient(cfg ClientConfig) Client { - return Client{ - client: http.Client{}, - config: cfg, +func NewClient(options ...ClientOption) Client { + c := Client{ + client: http.DefaultClient, + host: icp0, logger: new(NoopLogger), } -} - -// NewClientWithLogger creates a new client based on the given configuration and logger. -func NewClientWithLogger(cfg ClientConfig, logger Logger) Client { - if logger == nil { - logger = new(NoopLogger) - } - return Client{ - client: http.Client{}, - config: cfg, - logger: logger, + for _, o := range options { + o(&c) } + return c } func (c Client) Call(ctx context.Context, canisterID principal.Principal, data []byte) ([]byte, error) { @@ -52,16 +50,20 @@ func (c Client) Call(ctx context.Context, canisterID principal.Principal, data [ if err != nil { return nil, err } + defer resp.Body.Close() switch resp.StatusCode { case http.StatusAccepted: return io.ReadAll(resp.Body) case http.StatusOK: - body, _ := io.ReadAll(resp.Body) - var err preprocessingError - if err := cbor.Unmarshal(body, &err); err != nil { + body, err := io.ReadAll(resp.Body) + if err != nil { return nil, err } - return nil, fmt.Errorf("(%d) %s: %s", err.RejectCode, err.Message, err.ErrorCode) + var pErr preprocessingError + if err := cbor.Unmarshal(body, &pErr); err != nil { + return nil, err + } + return nil, pErr default: body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("(%d) %s: %s", resp.StatusCode, resp.Status, body) @@ -96,6 +98,7 @@ func (c Client) get(path string) ([]byte, error) { if err != nil { return nil, err } + defer resp.Body.Close() return io.ReadAll(resp.Body) } @@ -119,11 +122,15 @@ func (c Client) post(ctx context.Context, path string, canisterID principal.Prin if err != nil { return nil, err } + defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK: return io.ReadAll(resp.Body) default: - body, _ := io.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } return nil, fmt.Errorf("(%d) %s: %s", resp.StatusCode, resp.Status, body) } } @@ -139,24 +146,43 @@ func (c Client) postSubnet(ctx context.Context, path string, subnetID principal. if err != nil { return nil, err } + defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK: return io.ReadAll(resp.Body) default: - body, _ := io.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } return nil, fmt.Errorf("(%d) %s: %s", resp.StatusCode, resp.Status, body) } } func (c Client) url(p string) string { - u := *c.config.Host + u := *c.host u.Path = path.Join(u.Path, p) return u.String() } -// ClientConfig is the configuration for a client. -type ClientConfig struct { - Host *url.URL +type ClientOption func(c *Client) + +func WithHostURL(host *url.URL) ClientOption { + return func(c *Client) { + c.host = host + } +} + +func WithHttpClient(client *http.Client) ClientOption { + return func(c *Client) { + c.client = client + } +} + +func WithLogger(logger Logger) ClientOption { + return func(c *Client) { + c.logger = logger + } } type preprocessingError struct { @@ -167,3 +193,7 @@ type preprocessingError struct { // An optional implementation-specific textual error code. ErrorCode string `cbor:"error_code"` } + +func (e preprocessingError) Error() string { + return fmt.Sprintf("(%d) %s: %s", e.RejectCode, e.Message, e.ErrorCode) +} diff --git a/clients/registry/proto/v1/operator.pb.go b/clients/registry/proto/v1/operator.pb.go index 4daeb7e..abd9744 100644 --- a/clients/registry/proto/v1/operator.pb.go +++ b/clients/registry/proto/v1/operator.pb.go @@ -83,6 +83,31 @@ var file_operator_proto_rawDesc = []byte{ 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } +func file_operator_proto_init() { + if File_operator_proto != nil { + return + } + file_operator_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_operator_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_operator_proto_goTypes, + DependencyIndexes: file_operator_proto_depIdxs, + MessageInfos: file_operator_proto_msgTypes, + }.Build() + File_operator_proto = out.File + file_operator_proto_rawDesc = nil + file_operator_proto_goTypes = nil + file_operator_proto_depIdxs = nil +} + func file_operator_proto_rawDescGZIP() []byte { file_operator_proto_rawDescOnce.Do(func() { file_operator_proto_rawDescData = protoimpl.X.CompressGZIP(file_operator_proto_rawDescData) @@ -90,6 +115,8 @@ func file_operator_proto_rawDescGZIP() []byte { return file_operator_proto_rawDescData } +func init() { file_operator_proto_init() } + // A record for a node operator. Each node operator is associated with a // unique principal id, a.k.a. NOID. // @@ -208,9 +235,7 @@ func (x *RemoveNodeOperatorsPayload) GetNodeOperatorsToRemove() [][]byte { } return nil } - func (*RemoveNodeOperatorsPayload) ProtoMessage() {} - func (x *RemoveNodeOperatorsPayload) ProtoReflect() protoreflect.Message { mi := &file_operator_proto_msgTypes[1] if x != nil { @@ -222,6 +247,7 @@ func (x *RemoveNodeOperatorsPayload) ProtoReflect() protoreflect.Message { } return mi.MessageOf(x) } + func (x *RemoveNodeOperatorsPayload) Reset() { *x = RemoveNodeOperatorsPayload{} mi := &file_operator_proto_msgTypes[1] @@ -231,29 +257,3 @@ func (x *RemoveNodeOperatorsPayload) Reset() { func (x *RemoveNodeOperatorsPayload) String() string { return protoimpl.X.MessageStringOf(x) } - -func init() { file_operator_proto_init() } -func file_operator_proto_init() { - if File_operator_proto != nil { - return - } - file_operator_proto_msgTypes[0].OneofWrappers = []any{} - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_operator_proto_rawDesc, - NumEnums: 0, - NumMessages: 3, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_operator_proto_goTypes, - DependencyIndexes: file_operator_proto_depIdxs, - MessageInfos: file_operator_proto_msgTypes, - }.Build() - File_operator_proto = out.File - file_operator_proto_rawDesc = nil - file_operator_proto_goTypes = nil - file_operator_proto_depIdxs = nil -} diff --git a/gen/generator.go b/gen/generator.go index 76b93d8..588f35b 100644 --- a/gen/generator.go +++ b/gen/generator.go @@ -8,6 +8,7 @@ import ( "io/fs" "strings" "text/template" + "unicode" "github.com/aviate-labs/agent-go/candid/did" ) @@ -84,6 +85,9 @@ func NewGenerator(agentName, canisterName, packageName string, rawDID []rune) (* if err != nil { return nil, err } + if rs := []rune(agentName); unicode.IsLower(rs[0]) { + agentName = strings.ToUpper(agentName[:1]) + agentName[1:] + } return &Generator{ AgentName: agentName, CanisterName: canisterName, diff --git a/gen/generator_test.go b/gen/generator_test.go new file mode 100644 index 0000000..dc9643d --- /dev/null +++ b/gen/generator_test.go @@ -0,0 +1,124 @@ +package gen_test + +import ( + "fmt" + + "github.com/aviate-labs/agent-go/gen" +) + +func ExampleNewGenerator() { + g, err := gen.NewGenerator("test", "test", "test", []rune("service : { inc: () -> (nat) }")) + if err != nil { + panic(err) + } + raw, err := g.Generate() + if err != nil { + panic(err) + } + fmt.Println(string(raw)) + // Output: + // // Package test provides a client for the "test" canister. + // // Do NOT edit this file. It was automatically generated by https://github.com/aviate-labs/agent-go. + // package test + // + // import ( + // "github.com/aviate-labs/agent-go" + // "github.com/aviate-labs/agent-go/candid/idl" + // "github.com/aviate-labs/agent-go/principal" + // ) + // + // // TestAgent is a client for the "test" canister. + // type TestAgent struct { + // *agent.Agent + // CanisterId principal.Principal + // } + // + // // NewTestAgent creates a new agent for the "test" canister. + // func NewTestAgent(canisterId principal.Principal, config agent.Config) (*TestAgent, error) { + // a, err := agent.New(config) + // if err != nil { + // return nil, err + // } + // return &TestAgent{ + // Agent: a, + // CanisterId: canisterId, + // }, nil + // } + // + // // Inc calls the "inc" method on the "test" canister. + // func (a TestAgent) Inc() (*idl.Nat, error) { + // var r0 idl.Nat + // if err := a.Agent.Call( + // a.CanisterId, + // "inc", + // []any{}, + // []any{&r0}, + // ); err != nil { + // return nil, err + // } + // return &r0, nil + // } +} + +func ExampleNewGenerator_indirect() { + g, err := gen.NewGenerator("test", "test", "test", []rune("service : { inc: () -> (nat) }")) + if err != nil { + panic(err) + } + raw, err := g.Indirect().Generate() + if err != nil { + panic(err) + } + fmt.Println(string(raw)) + // Output: + // // Package test provides a client for the "test" canister. + // // Do NOT edit this file. It was automatically generated by https://github.com/aviate-labs/agent-go. + // package test + // + // import ( + // "github.com/aviate-labs/agent-go" + // "github.com/aviate-labs/agent-go/candid/idl" + // "github.com/aviate-labs/agent-go/principal" + // ) + // + // // TestAgent is a client for the "test" canister. + // type TestAgent struct { + // *agent.Agent + // CanisterId principal.Principal + // } + // + // // NewTestAgent creates a new agent for the "test" canister. + // func NewTestAgent(canisterId principal.Principal, config agent.Config) (*TestAgent, error) { + // a, err := agent.New(config) + // if err != nil { + // return nil, err + // } + // return &TestAgent{ + // Agent: a, + // CanisterId: canisterId, + // }, nil + // } + // + // // Inc calls the "inc" method on the "test" canister. + // func (a TestAgent) Inc() (*idl.Nat, error) { + // var r0 idl.Nat + // if err := a.Agent.Call( + // a.CanisterId, + // "inc", + // []any{}, + // []any{&r0}, + // ); err != nil { + // return nil, err + // } + // return &r0, nil + // } + // + // // IncCall creates an indirect representation of the "inc" method on the "test" canister. + // func (a TestAgent) IncCall() (*agent.CandidAPIRequest, error) { + // return a.Agent.CreateCandidAPIRequest( + // agent.RequestTypeCall, + // a.CanisterId, + // "inc", + // ) + // } +} diff --git a/gen/templates/agent.gotmpl b/gen/templates/agent.gotmpl index ac9442f..8087849 100644 --- a/gen/templates/agent.gotmpl +++ b/gen/templates/agent.gotmpl @@ -3,8 +3,8 @@ package {{ .PackageName }} import ( - "github.com/aviate-labs/agent-go" - {{ if .UsedIDL }}"github.com/aviate-labs/agent-go/candid/idl"{{ end }} + "github.com/aviate-labs/agent-go"{{ if .UsedIDL }} + "github.com/aviate-labs/agent-go/candid/idl"{{ end }} "github.com/aviate-labs/agent-go/principal" ) diff --git a/gen/templates/agent_indirect.gotmpl b/gen/templates/agent_indirect.gotmpl index cc4c63d..ec132ac 100644 --- a/gen/templates/agent_indirect.gotmpl +++ b/gen/templates/agent_indirect.gotmpl @@ -3,8 +3,8 @@ package {{ .PackageName }} import ( - "github.com/aviate-labs/agent-go" - {{ if .UsedIDL }}"github.com/aviate-labs/agent-go/candid/idl"{{ end }} + "github.com/aviate-labs/agent-go"{{ if .UsedIDL }} + "github.com/aviate-labs/agent-go/candid/idl"{{ end }} "github.com/aviate-labs/agent-go/principal" ) diff --git a/status_test.go b/status_test.go index b7bfbb1..b7a1a26 100644 --- a/status_test.go +++ b/status_test.go @@ -10,7 +10,7 @@ import ( var ic0URL, _ = url.Parse("https://icp-api.io") func ExampleClient_Status() { - c := agent.NewClient(agent.ClientConfig{Host: ic0URL}) + c := agent.NewClient(agent.WithHostURL(ic0URL)) status, _ := c.Status() fmt.Printf("%x...%x\n", status.RootKey[:4], status.RootKey[len(status.RootKey)-4:]) // Output: