[agent/codex:gpt-5.3-codex-spark] Read docs/RFC.md fully. Find ONE feature described in the sp... #11
3 changed files with 408 additions and 0 deletions
244
mainchain.go
Normal file
244
mainchain.go
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MainchainClientOptions struct {
|
||||
URL string
|
||||
Username string
|
||||
Password string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
type MainchainAliasClient struct {
|
||||
baseURL string
|
||||
username string
|
||||
password string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type MainchainRPCRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
Method string `json:"method"`
|
||||
Params []any `json:"params"`
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
type MainchainRPCResponse struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
Error *HSDRPCError `json:"error"`
|
||||
}
|
||||
|
||||
func NewMainchainAliasClient(options MainchainClientOptions) *MainchainAliasClient {
|
||||
client := options.HTTPClient
|
||||
if client == nil {
|
||||
client = &http.Client{}
|
||||
}
|
||||
|
||||
baseURL := strings.TrimSpace(options.URL)
|
||||
if baseURL == "" {
|
||||
baseURL = "http://127.0.0.1:14037"
|
||||
}
|
||||
|
||||
return &MainchainAliasClient{
|
||||
baseURL: baseURL,
|
||||
username: options.Username,
|
||||
password: options.Password,
|
||||
httpClient: client,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllAliasDetails returns alias names mapped to HNS DNS names.
|
||||
//
|
||||
// client := dns.NewMainchainAliasClient(dns.MainchainClientOptions{
|
||||
// URL: "http://127.0.0.1:14037",
|
||||
// Username: "user",
|
||||
// Password: "pass",
|
||||
// })
|
||||
// aliases, err := client.GetAllAliasDetails(context.Background())
|
||||
func (client *MainchainAliasClient) GetAllAliasDetails(ctx context.Context) ([]string, error) {
|
||||
request := MainchainRPCRequest{
|
||||
JSONRPC: defaultHSDJSONRPCVersion,
|
||||
Method: "get_all_alias_details",
|
||||
Params: []any{},
|
||||
ID: 1,
|
||||
}
|
||||
|
||||
raw, err := client.call(ctx, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aliases, err := parseMainchainAliases(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aliases, nil
|
||||
}
|
||||
|
||||
func (client *MainchainAliasClient) call(ctx context.Context, request MainchainRPCRequest) (json.RawMessage, error) {
|
||||
body, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, client.baseURL, io.NopCloser(io.Reader(strings.NewReader(string(body)))))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if client.username != "" || client.password != "" {
|
||||
httpRequest.Header.Set("Authorization", "Basic "+basicAuthToken(client.username, client.password))
|
||||
}
|
||||
|
||||
response, err := client.httpClient.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = response.Body.Close() }()
|
||||
|
||||
responseBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("mainchain rpc request failed with status %d: %s", response.StatusCode, strings.TrimSpace(string(responseBody)))
|
||||
}
|
||||
|
||||
var decoded MainchainRPCResponse
|
||||
if err := json.Unmarshal(responseBody, &decoded); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if decoded.Error != nil {
|
||||
return nil, decoded.Error
|
||||
}
|
||||
|
||||
return decoded.Result, nil
|
||||
}
|
||||
|
||||
func parseMainchainAliases(raw json.RawMessage) ([]string, error) {
|
||||
var result []string
|
||||
if err := json.Unmarshal(raw, &result); err == nil {
|
||||
return normalizeAliasList(result), nil
|
||||
}
|
||||
|
||||
var wrappedResult map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &wrappedResult); err == nil {
|
||||
if aliasesRaw, ok := wrappedResult["aliases"]; ok {
|
||||
return parseMainchainAliases(aliasesRaw)
|
||||
}
|
||||
if resultRaw, ok := wrappedResult["result"]; ok {
|
||||
return parseMainchainAliases(resultRaw)
|
||||
}
|
||||
}
|
||||
|
||||
var rawRecords []json.RawMessage
|
||||
if err := json.Unmarshal(raw, &rawRecords); err != nil {
|
||||
return nil, errors.New("unable to parse get_all_alias_details result")
|
||||
}
|
||||
|
||||
parsed := make([]string, 0, len(rawRecords))
|
||||
for _, rawRecord := range rawRecords {
|
||||
next, err := parseMainchainAliasRecord(rawRecord)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if next != "" {
|
||||
parsed = append(parsed, next)
|
||||
}
|
||||
}
|
||||
return normalizeAliasList(parsed), nil
|
||||
}
|
||||
|
||||
func parseMainchainAliasRecord(raw json.RawMessage) (string, error) {
|
||||
var name string
|
||||
if err := json.Unmarshal(raw, &name); err == nil {
|
||||
return normalizeName(name), nil
|
||||
}
|
||||
|
||||
var record map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &record); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var candidate string
|
||||
if value, ok := record["hns"]; ok {
|
||||
if hns, ok := decodeString(value); ok {
|
||||
candidate = normalizeName(hns)
|
||||
}
|
||||
}
|
||||
if candidate == "" && record["comment"] != nil {
|
||||
if comment, ok := decodeString(record["comment"]); ok {
|
||||
candidate = extractAliasFromComment(comment)
|
||||
}
|
||||
}
|
||||
if candidate == "" && record["alias"] != nil {
|
||||
if alias, ok := decodeString(record["alias"]); ok {
|
||||
candidate = normalizeName(alias)
|
||||
}
|
||||
}
|
||||
if candidate == "" && record["name"] != nil {
|
||||
if alias, ok := decodeString(record["name"]); ok {
|
||||
candidate = normalizeName(alias)
|
||||
}
|
||||
}
|
||||
return candidate, nil
|
||||
}
|
||||
|
||||
func decodeString(raw json.RawMessage) (string, bool) {
|
||||
var value string
|
||||
if len(raw) == 0 {
|
||||
return "", false
|
||||
}
|
||||
if err := json.Unmarshal(raw, &value); err != nil {
|
||||
return "", false
|
||||
}
|
||||
value = strings.TrimSpace(value)
|
||||
return value, value != ""
|
||||
}
|
||||
|
||||
func extractAliasFromComment(comment string) string {
|
||||
for _, token := range strings.Fields(comment) {
|
||||
if strings.HasPrefix(token, "hns=") {
|
||||
return normalizeName(strings.TrimSuffix(strings.TrimPrefix(token, "hns="), ";"))
|
||||
}
|
||||
}
|
||||
|
||||
if marker := strings.Index(comment, "hns="); marker >= 0 {
|
||||
alias := comment[marker+4:]
|
||||
if trim := strings.IndexAny(alias, " ;,"); trim >= 0 {
|
||||
alias = alias[:trim]
|
||||
}
|
||||
alias = strings.TrimSpace(alias)
|
||||
alias = strings.TrimSuffix(alias, ";")
|
||||
return normalizeName(alias)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeAliasList(raw []string) []string {
|
||||
seen := map[string]bool{}
|
||||
normalized := make([]string, 0, len(raw))
|
||||
for _, name := range raw {
|
||||
next := normalizeName(name)
|
||||
if next == "" {
|
||||
continue
|
||||
}
|
||||
if seen[next] {
|
||||
continue
|
||||
}
|
||||
seen[next] = true
|
||||
normalized = append(normalized, next)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
142
mainchain_test.go
Normal file
142
mainchain_test.go
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMainchainClientGetsAliasDetailsAndParsesHNSComments(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
|
||||
var payload struct {
|
||||
Method string `json:"method"`
|
||||
}
|
||||
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("unexpected request payload: %v", err)
|
||||
}
|
||||
if payload.Method != "get_all_alias_details" {
|
||||
t.Fatalf("expected method get_all_alias_details, got %s", payload.Method)
|
||||
}
|
||||
|
||||
responseWriter.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(responseWriter).Encode(map[string]any{
|
||||
"result": []any{
|
||||
map[string]any{
|
||||
"name": "gateway",
|
||||
"comment": "gateway alias hns=gateway.charon.lthn",
|
||||
},
|
||||
map[string]any{
|
||||
"hns": "node.charon.lthn",
|
||||
"name": "node",
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewMainchainAliasClient(MainchainClientOptions{
|
||||
URL: server.URL,
|
||||
})
|
||||
|
||||
aliases, err := client.GetAllAliasDetails(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected get_all_alias_details error: %v", err)
|
||||
}
|
||||
if len(aliases) != 2 {
|
||||
t.Fatalf("expected 2 aliases, got %d", len(aliases))
|
||||
}
|
||||
if aliases[0] != "gateway.charon.lthn" || aliases[1] != "node.charon.lthn" {
|
||||
t.Fatalf("unexpected aliases: %#v", aliases)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceDiscoverFromMainchainAliasesUsesMainchainThenHSD(t *testing.T) {
|
||||
var chainCalls int32
|
||||
var hsdTreeRootCalls int32
|
||||
var hsdAliasCalls int32
|
||||
|
||||
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 "get_all_alias_details":
|
||||
atomic.AddInt32(&chainCalls, 1)
|
||||
responseWriter.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(responseWriter).Encode(map[string]any{
|
||||
"result": []any{
|
||||
map[string]any{
|
||||
"hns": "gateway.charon.lthn",
|
||||
},
|
||||
map[string]any{
|
||||
"hns": "node.charon.lthn",
|
||||
},
|
||||
},
|
||||
})
|
||||
case "getblockchaininfo":
|
||||
atomic.AddInt32(&hsdTreeRootCalls, 1)
|
||||
responseWriter.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(responseWriter).Encode(map[string]any{
|
||||
"result": map[string]any{
|
||||
"tree_root": "chain-root-1",
|
||||
},
|
||||
})
|
||||
case "getnameresource":
|
||||
atomic.AddInt32(&hsdAliasCalls, 1)
|
||||
responseWriter.Header().Set("Content-Type", "application/json")
|
||||
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{})
|
||||
chainClient := NewMainchainAliasClient(MainchainClientOptions{
|
||||
URL: server.URL,
|
||||
})
|
||||
hsdClient := NewHSDClient(HSDClientOptions{
|
||||
URL: server.URL,
|
||||
})
|
||||
|
||||
if err := service.DiscoverFromMainchainAliases(context.Background(), chainClient, hsdClient); err != nil {
|
||||
t.Fatalf("expected discover from mainchain aliases: %v", err)
|
||||
}
|
||||
|
||||
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 chainCalls != 1 || hsdTreeRootCalls != 1 || hsdAliasCalls != 2 {
|
||||
t.Fatalf("expected chain=1 tree-root=1 name-resource=2, got %d %d %d", chainCalls, hsdTreeRootCalls, hsdAliasCalls)
|
||||
}
|
||||
}
|
||||
|
||||
22
service.go
22
service.go
|
|
@ -93,6 +93,28 @@ func (service *Service) DiscoverFromChainAliases(ctx context.Context, client *HS
|
|||
return service.discoverFromChainAliasesUsingTreeRoot(ctx, aliases, client)
|
||||
}
|
||||
|
||||
// DiscoverFromMainchainAliases updates records from main-chain aliases resolved through HSD.
|
||||
//
|
||||
// service.DiscoverFromMainchainAliases(context.Background(), dns.NewMainchainAliasClient(dns.MainchainClientOptions{
|
||||
// URL: "http://127.0.0.1:14037",
|
||||
// }), dns.NewHSDClient(dns.HSDClientOptions{
|
||||
// URL: "http://127.0.0.1:14037",
|
||||
// }))
|
||||
func (service *Service) DiscoverFromMainchainAliases(ctx context.Context, chainClient *MainchainAliasClient, hsdClient *HSDClient) error {
|
||||
if chainClient == nil {
|
||||
return fmt.Errorf("mainchain alias client is required")
|
||||
}
|
||||
|
||||
aliases, err := chainClient.GetAllAliasDetails(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(aliases) == 0 {
|
||||
return nil
|
||||
}
|
||||
return service.discoverFromChainAliasesUsingTreeRoot(ctx, aliases, hsdClient)
|
||||
}
|
||||
|
||||
func (service *Service) discoverFromChainAliasesUsingTreeRoot(ctx context.Context, aliases []string, client *HSDClient) error {
|
||||
if len(aliases) == 0 {
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue