diff --git a/go/cmd/cli.go b/go/cmd/cli.go new file mode 100644 index 0000000..0de8694 --- /dev/null +++ b/go/cmd/cli.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io" + "os" + + "github.com/paradigmxyz/mesc/go/pkg/mesc" + "github.com/paradigmxyz/mesc/go/pkg/mesc/endpoint/io/serialization" +) + +func main() { + ctx := context.Background() + + var queryType string + flag.StringVar(&queryType, "query-type", "", "the type of query to execute") + + flag.Parse() + + switch queryType { + case "get_default_endpoint": + endpoint, err := mesc.GetDefaultEndpoint(ctx) + if err != nil { + printFailure(fmt.Errorf("failed to get default endpoint: %w", err)) + return + } + + jsonModel, err := serialization.SerializeEndpointMetadataJSON(endpoint) + if err != nil { + printFailure(fmt.Errorf("failed to serialize endpoint metadata to JSON: %w", err)) + return + } + + _, _ = io.Copy(os.Stdout, jsonModel) + + return + } +} + +func printFailure(e error) { + fmt.Printf("FAIL: %v\n", e) +} diff --git a/go/pkg/mesc/endpoint/io/serialization/json.go b/go/pkg/mesc/endpoint/io/serialization/json.go index 9e1f0fb..57baf62 100644 --- a/go/pkg/mesc/endpoint/io/serialization/json.go +++ b/go/pkg/mesc/endpoint/io/serialization/json.go @@ -1,6 +1,7 @@ package serialization import ( + "bytes" "encoding/json" "fmt" "io" @@ -34,6 +35,54 @@ func DeserializeEndpointMetadataJSON(reader io.Reader) (map[string]model.Endpoin return endpoints, nil } +// SerializeJSON serializes the given RPC configuration to a JSON representation conforming to the MESC specification. +func SerializeJSON(rpcConfig *model.RPCConfig) (io.Reader, error) { + jsonProfiles := make(map[string]*jsonProfile) + for profileKey, profile := range jsonProfiles { + jsonProfiles[profileKey] = &jsonProfile{ + Name: profile.Name, + DefaultEndpoint: profile.DefaultEndpoint, + NetworkDefaults: profile.NetworkDefaults, + ProfileMetadata: profile.ProfileMetadata, + UseMESC: profile.UseMESC, + } + } + + jsonEndpoints := make(map[string]*jsonEndpoint) + for endpointKey, endpoint := range rpcConfig.Endpoints { + jsonEndpoints[endpointKey] = modelEndpointToJSON(&endpoint) + } + + jsonRPCConfig := &jsonRPCConfig{ + MESCVersion: rpcConfig.MESCVersion, + DefaultEndpoint: rpcConfig.DefaultEndpoint, + NetworkDefaults: fromChainIDMap(rpcConfig.NetworkDefaults), + NetworkNames: fromMappedChainID(rpcConfig.NetworkNames), + Endpoints: jsonEndpoints, + Profiles: jsonProfiles, + GlobalMetadata: rpcConfig.GlobalMetadata, + } + + jsonBytes, err := json.Marshal(jsonRPCConfig) + if err != nil { + return nil, fmt.Errorf("failed to marshal RPC config to bytes: %w", err) + } + + return bytes.NewBuffer(jsonBytes), nil +} + +// SerializeEndpointMetadataJSON serializes the given endpoint model to a JSON form compliant with the MESC specification. +func SerializeEndpointMetadataJSON(endpoint *model.EndpointMetadata) (io.Reader, error) { + jsonEndpoint := modelEndpointToJSON(endpoint) + + jsonBytes, err := json.Marshal(jsonEndpoint) + if err != nil { + return nil, fmt.Errorf("failed to marshal endpoint metadata to bytes: %w", err) + } + + return bytes.NewBuffer(jsonBytes), nil +} + func toOptionalChainID(v *string) *model.ChainID { if v == nil { return nil @@ -43,6 +92,32 @@ func toOptionalChainID(v *string) *model.ChainID { return &asModel } +func fromChainIDMap[V any](source map[model.ChainID]V) map[string]V { + if source == nil { + return nil + } + + copy := make(map[string]V, len(source)) + for chainID, value := range source { + copy[string(chainID)] = value + } + + return copy +} + +func fromMappedChainID[K comparable](source map[K]model.ChainID) map[K]string { + if source == nil { + return nil + } + + copy := make(map[K]string, len(source)) + for key, chainID := range source { + copy[key] = string(chainID) + } + + return copy +} + func toChainIDMap[V any](source map[string]V) map[model.ChainID]V { if source == nil { return nil @@ -70,13 +145,13 @@ func toMappedChainID[K comparable](source map[K]string) map[K]model.ChainID { } type jsonRPCConfig struct { - MESCVersion string `json:"mesc_version"` - DefaultEndpoint *string `json:"default_endpoint"` - NetworkDefaults map[string]string `json:"network_defaults"` - NetworkNames map[string]string `json:"network_names"` - Endpoints map[string]*jsonEndpoint - Profiles map[string]*jsonProfile - GlobalMetadata map[string]any `json:"global_metadata"` + MESCVersion string `json:"mesc_version"` + DefaultEndpoint *string `json:"default_endpoint"` + NetworkDefaults map[string]string `json:"network_defaults"` + NetworkNames map[string]string `json:"network_names"` + Endpoints map[string]*jsonEndpoint `json:"endpoints"` + Profiles map[string]*jsonProfile `json:"profiles"` + GlobalMetadata map[string]any `json:"global_metadata"` } func (j *jsonRPCConfig) toModel() *model.RPCConfig { @@ -110,9 +185,24 @@ func (j *jsonRPCConfig) toModel() *model.RPCConfig { return rpcConfig } +func modelEndpointToJSON(endpoint *model.EndpointMetadata) *jsonEndpoint { + var chainIDPtr *string + if endpoint.ChainID != nil { + v := string(*endpoint.ChainID) + chainIDPtr = &v + } + + return &jsonEndpoint{ + Name: endpoint.Name, + URL: endpoint.URL, + ChainID: chainIDPtr, + EndpointMetadata: endpoint.EndpointMetadata, + } +} + type jsonEndpoint struct { - Name string - URL string + Name string `json:"name"` + URL string `json:"url"` ChainID *string `json:"chain_id"` EndpointMetadata map[string]any `json:"endpoint_metadata"` } @@ -127,7 +217,7 @@ func (j *jsonEndpoint) toModel() model.EndpointMetadata { } type jsonProfile struct { - Name string + Name string `json:"name"` DefaultEndpoint *string `json:"default_endpoint"` NetworkDefaults map[string]string `json:"network_defaults"` ProfileMetadata map[string]any `json:"profile_metadata"` diff --git a/go/pkg/mesc/endpoint/resolution/config.go b/go/pkg/mesc/endpoint/resolution/config.go index ccd1e60..bd695bd 100644 --- a/go/pkg/mesc/endpoint/resolution/config.go +++ b/go/pkg/mesc/endpoint/resolution/config.go @@ -8,6 +8,26 @@ type EndpointResolutionConfig struct { rpcConfig *model.RPCConfig } +// GetProfile gets the profile, if any, that was supplied. +// It returns true for the bool if there is a profile supplied; false if not. +func (e *EndpointResolutionConfig) GetProfile() (string, bool) { + if e.profile == nil { + return "", false + } + + return *e.profile, true +} + +// GetRPCConfig gets the RPC configuration, if any, that was supplied. +// It returns false for the bool if there is an RPC configuration supplied; false if not. +func (e *EndpointResolutionConfig) GetRPCConfig() (model.RPCConfig, bool) { + if e.rpcConfig == nil { + return model.RPCConfig{}, false + } + + return *e.rpcConfig, true +} + // EndpointResolutionOption describes a way of configuring the resolution of an endpoint type EndpointResolutionOption func(*EndpointResolutionConfig) diff --git a/go/pkg/mesc/endpoints.go b/go/pkg/mesc/endpoints.go index 2277a98..c157d72 100644 --- a/go/pkg/mesc/endpoints.go +++ b/go/pkg/mesc/endpoints.go @@ -3,6 +3,7 @@ package mesc import ( "context" "errors" + "fmt" criteria "github.com/paradigmxyz/mesc/go/pkg/mesc/endpoint/criteria" resolution "github.com/paradigmxyz/mesc/go/pkg/mesc/endpoint/resolution" @@ -20,8 +21,43 @@ func FindEndpoints(ctx context.Context, findCriteria []criteria.FindEndpointsCri // GetDefaultEndpoint resolves the endpoint metadata, if available, for the default endpoint. // This will return nil if no endpoint metadata can be resolved. func GetDefaultEndpoint(ctx context.Context, options ...resolution.EndpointResolutionOption) (*model.EndpointMetadata, error) { - // TODO: implement - return nil, errors.New("not yet implemented") + resolutionConfig := resolveEndpointResolutionConfig(options...) + + rpcConfig, hasConfig := resolutionConfig.GetRPCConfig() + if !hasConfig { + resolvedRPCConfig, err := ResolveRPCConfig(ctx) + if err != nil { + return nil, fmt.Errorf("failed to resolve RPC configuration: %w", err) + } + + rpcConfig = *resolvedRPCConfig + } + + var defaultEndpointName string + + profileName, hasProfileName := resolutionConfig.GetProfile() + if hasProfileName { + profile, hasProfile := rpcConfig.Profiles[profileName] + if hasProfile && profile.DefaultEndpoint != nil { + defaultEndpointName = *profile.DefaultEndpoint + } + } + + if defaultEndpointName == "" && rpcConfig.DefaultEndpoint != nil { + defaultEndpointName = *rpcConfig.DefaultEndpoint + } + + if defaultEndpointName == "" { + // unable to resolve default endpoint name to use, so nothing can be found + return nil, nil + } + + endpoint, hasEndpoint := rpcConfig.Endpoints[defaultEndpointName] + if !hasEndpoint { + return nil, nil + } + + return &endpoint, nil } // GetEndpointByNetwork resolves the endpoint metadata, if available, for the given chain ID. @@ -52,3 +88,12 @@ func IsMESCEnabled(ctx context.Context) (bool, error) { // TODO: implement return false, errors.New("not yet implemented") } + +func resolveEndpointResolutionConfig(options ...resolution.EndpointResolutionOption) *resolution.EndpointResolutionConfig { + cfg := &resolution.EndpointResolutionConfig{} + for _, option := range options { + option(cfg) + } + + return cfg +} diff --git a/tests/adapters/golang b/tests/adapters/golang new file mode 100644 index 0000000..5747a21 --- /dev/null +++ b/tests/adapters/golang @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import typing +from typing import cast + +if typing.TYPE_CHECKING: + from mesc.types import MescQuery + + +def run_query(query: MescQuery) -> str: + if query["query_type"] == "default_endpoint": + cmd: list[str] = ["go", "run", "../go/cmd/cli.go", "--query-type", "get_default_endpoint"] + # elif query["query_type"] == "endpoint_by_name": + # name = cast(str, query["fields"]["name"]) # type: ignore + # cmd = ["mesc", "endpoint", "--json", "--name", name] + # elif query["query_type"] == "endpoint_by_network": + # chain_id = query["fields"]["chain_id"] + # if isinstance(chain_id, int): + # chain_id = str(chain_id) + # cmd = ["mesc", "endpoint", "--json", "--network", chain_id] + # elif query["query_type"] == "user_input": + # user_input = query["fields"]["user_input"] # type: ignore + # cmd = ["mesc", "endpoint", user_input, "--json"] + # elif query["query_type"] == "multi_endpoint": + # cmd = ["mesc", "ls", "--json"] + # if query["fields"].get('name_contains') is not None: + # cmd.append('--name') + # cmd.append(query['fields']['name_contains']) + # if query["fields"].get('url_contains') is not None: + # cmd.append('--url') + # cmd.append(query['fields']['url_contains']) + # if query["fields"].get('chain_id') is not None: + # cmd.append('--network') + # chain_id = query["fields"]["chain_id"] + # if isinstance(chain_id, int): + # chain_id = str(chain_id) + # cmd.append(chain_id) + # elif query['query_type'] == 'global_metadata': + # cmd = ["mesc", "metadata"] + else: + raise Exception("invalid query query_type: " + str(query["query_type"])) + + if query["fields"].get("profile") is not None: + cmd.append("--profile") + cmd.append(query["fields"]["profile"]) # type: ignore + + raw_output = subprocess.check_output(cmd, env=dict(os.environ), stderr=subprocess.DEVNULL) + output = raw_output.decode("utf-8").strip() + if output == "": + output = "null" + + return output + + +if __name__ == "__main__": + # load test + parser = argparse.ArgumentParser() + parser.add_argument("test") + args = parser.parse_args() + test = json.loads(args.test) + + # run test + try: + raw_output = run_query(test) + try: + result = json.loads(raw_output) + print(json.dumps(result, indent=4, sort_keys=True)) + except Exception as e: + print("FAIL") + print(e) + print('RAW_OUTPUT:', raw_output) + except Exception as e: + print("FAIL") + print(e) + diff --git a/tests/go.work b/tests/go.work new file mode 100644 index 0000000..3bb1afa --- /dev/null +++ b/tests/go.work @@ -0,0 +1,4 @@ +go 1.21.3 + +// Needed so that 'go run' works from this directory +use ../go \ No newline at end of file