diff --git a/action.go b/action.go index 3a4e4f2..f0c10fb 100644 --- a/action.go +++ b/action.go @@ -35,6 +35,13 @@ type ActionRegistrar interface { RegisterAction(name string, invoke func(map[string]any) (any, bool, error)) } +// ActionCaller resolves named actions from another Core surface. +// +// payload, ok, err := caller.CallAction(context.Background(), "blockchain.chain.aliases", nil) +type ActionCaller interface { + CallAction(ctx context.Context, name string, values map[string]any) (any, bool, error) +} + // ActionDefinitions returns the complete DNS action surface in registration order. // // definitions := service.ActionDefinitions() diff --git a/service.go b/service.go index 5a483a9..66c6e65 100644 --- a/service.go +++ b/service.go @@ -50,6 +50,7 @@ type Service struct { zoneApex string hsdClient *HSDClient mainchainAliasClient *MainchainAliasClient + chainAliasActionCaller ActionCaller chainAliasAction func(context.Context) ([]string, error) discoverer func() (map[string]NameRecords, error) fallbackDiscoverer func() (map[string]NameRecords, error) @@ -66,6 +67,7 @@ type ServiceOptions struct { FallbackDiscoverer func() (map[string]NameRecords, error) MainchainAliasClient *MainchainAliasClient HSDClient *HSDClient + ChainAliasActionCaller ActionCaller ChainAliasAction func(context.Context) ([]string, error) ChainAliasDiscoverer func(context.Context) ([]string, error) FallbackChainAliasDiscoverer func(context.Context) ([]string, error) @@ -106,6 +108,7 @@ func NewService(options ServiceOptions) *Service { zoneApex: computeZoneApex(cached), hsdClient: options.HSDClient, mainchainAliasClient: options.MainchainAliasClient, + chainAliasActionCaller: options.ChainAliasActionCaller, chainAliasAction: options.ChainAliasAction, discoverer: options.Discoverer, fallbackDiscoverer: options.FallbackDiscoverer, @@ -133,6 +136,7 @@ func (service *Service) DiscoverFromChainAliases(ctx context.Context, client *HS aliases, err := service.discoverAliasesFromSources( ctx, + service.chainAliasActionCaller, service.chainAliasAction, service.chainAliasDiscoverer, service.fallbackChainAliasDiscoverer, @@ -149,11 +153,16 @@ func (service *Service) DiscoverFromChainAliases(ctx context.Context, client *HS func (service *Service) discoverAliasesFromSources( ctx context.Context, + actionCaller ActionCaller, action func(context.Context) ([]string, error), discoverer func(context.Context) ([]string, error), fallback func(context.Context) ([]string, error), mainchainClient *MainchainAliasClient, ) ([]string, error) { + if aliases, ok := service.discoverAliasesFromActionCaller(ctx, actionCaller); ok { + return aliases, nil + } + if action != nil { aliases, err := action(ctx) if err == nil { @@ -199,6 +208,51 @@ func (service *Service) discoverAliasesFromSources( return mainchainClient.GetAllAliasDetails(ctx) } +func (service *Service) discoverAliasesFromActionCaller(ctx context.Context, actionCaller ActionCaller) ([]string, bool) { + if actionCaller == nil { + return nil, false + } + + result, ok, err := actionCaller.CallAction(ctx, "blockchain.chain.aliases", map[string]any{}) + if err != nil || !ok { + return nil, false + } + + aliases, err := parseActionAliasList(result) + if err != nil { + return nil, false + } + return aliases, true +} + +func parseActionAliasList(value any) ([]string, error) { + switch aliases := value.(type) { + case nil: + return nil, fmt.Errorf("blockchain.chain.aliases action returned no value") + case []string: + return normalizeAliasList(aliases), nil + case []any: + parsed := make([]string, 0, len(aliases)) + for _, item := range aliases { + name, ok := item.(string) + if !ok { + return nil, fmt.Errorf("blockchain.chain.aliases action returned non-string alias") + } + parsed = append(parsed, name) + } + return normalizeAliasList(parsed), nil + case map[string]any: + if rawAliases, ok := aliases["aliases"]; ok { + return parseActionAliasList(rawAliases) + } + if rawResult, ok := aliases["result"]; ok { + return parseActionAliasList(rawResult) + } + } + + return nil, fmt.Errorf("blockchain.chain.aliases action returned unsupported result type %T", value) +} + // DiscoverFromMainchainAliases updates records from main-chain aliases resolved through HSD. // // service.DiscoverFromMainchainAliases(context.Background(), dns.NewMainchainAliasClient(dns.MainchainClientOptions{ @@ -219,6 +273,7 @@ func (service *Service) DiscoverFromMainchainAliases(ctx context.Context, chainC aliases, err := service.discoverAliasesFromSources( ctx, + service.chainAliasActionCaller, nil, func(ctx context.Context) ([]string, error) { if service.chainAliasDiscoverer != nil { diff --git a/service_test.go b/service_test.go index 67dd0ba..173c1c9 100644 --- a/service_test.go +++ b/service_test.go @@ -516,6 +516,95 @@ func TestServiceDiscoverAliasesUsesConfiguredAliasDiscovery(t *testing.T) { } } +func TestServiceDiscoverAliasesUsesConfiguredActionCaller(t *testing.T) { + var treeRootCalls int32 + var nameResourceCalls int32 + actionCalled := false + discovererCalled := false + + server := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { + var payload struct { + Method string `json:"method"` + Params []any `json:"params"` + } + if err := json.NewDecoder(request.Body).Decode(&payload); err != nil { + t.Fatalf("unexpected request payload: %v", err) + } + + switch payload.Method { + case "getblockchaininfo": + atomic.AddInt32(&treeRootCalls, 1) + responseWriter.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(responseWriter).Encode(map[string]any{ + "result": map[string]any{ + "tree_root": "action-caller-root", + }, + }) + case "getnameresource": + atomic.AddInt32(&nameResourceCalls, 1) + switch payload.Params[0] { + case "gateway.charon.lthn": + _ = json.NewEncoder(responseWriter).Encode(map[string]any{ + "result": map[string]any{ + "a": []string{"10.10.10.10"}, + }, + }) + case "node.charon.lthn": + _ = json.NewEncoder(responseWriter).Encode(map[string]any{ + "result": map[string]any{ + "aaaa": []string{"2600:1f1c:7f0:4f01::2"}, + }, + }) + default: + t.Fatalf("unexpected alias lookup: %#v", payload.Params) + } + default: + t.Fatalf("unexpected method: %s", payload.Method) + } + })) + defer server.Close() + + service := NewService(ServiceOptions{ + ChainAliasActionCaller: actionCallerFunc(func(ctx context.Context, name string, values map[string]any) (any, bool, error) { + actionCalled = true + if name != "blockchain.chain.aliases" { + t.Fatalf("unexpected action name: %s", name) + } + return map[string]any{ + "aliases": []any{"gateway.charon.lthn", "node.charon.lthn"}, + }, true, nil + }), + ChainAliasDiscoverer: func(_ context.Context) ([]string, error) { + discovererCalled = true + return nil, errors.New("discoverer should not run when action caller succeeds") + }, + HSDClient: NewHSDClient(HSDClientOptions{URL: server.URL}), + }) + + if err := service.DiscoverAliases(context.Background()); err != nil { + t.Fatalf("expected DiscoverAliases to complete through action caller: %v", err) + } + if !actionCalled { + t.Fatal("expected action caller to be invoked") + } + if discovererCalled { + t.Fatal("expected chain alias discoverer to be skipped after action caller success") + } + + gateway, ok := service.Resolve("gateway.charon.lthn") + if !ok || len(gateway.A) != 1 || gateway.A[0] != "10.10.10.10" { + t.Fatalf("expected gateway A record, got %#v (ok=%t)", gateway, ok) + } + node, ok := service.Resolve("node.charon.lthn") + if !ok || len(node.AAAA) != 1 || node.AAAA[0] != "2600:1f1c:7f0:4f01::2" { + t.Fatalf("expected node AAAA record, got %#v (ok=%t)", node, ok) + } + + if atomic.LoadInt32(&treeRootCalls) != 1 || atomic.LoadInt32(&nameResourceCalls) != 2 { + t.Fatalf("expected discovery to perform chain and name-resource calls, got treeRoot=%d nameResource=%d", atomic.LoadInt32(&treeRootCalls), atomic.LoadInt32(&nameResourceCalls)) + } +} + func TestServiceDiscoverAliasesUsesConfiguredChainAliasAction(t *testing.T) { var treeRootCalls int32 var nameResourceCalls int32 @@ -1608,3 +1697,9 @@ func (recorder *actionRecorder) RegisterAction(name string, invoke func(map[stri recorder.names = append(recorder.names, name) recorder.handlers[name] = invoke } + +type actionCallerFunc func(context.Context, string, map[string]any) (any, bool, error) + +func (caller actionCallerFunc) CallAction(ctx context.Context, name string, values map[string]any) (any, bool, error) { + return caller(ctx, name, values) +}