From 1176d12b0a05a778c2351bc58b0c858d7dcc7bab Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 14 Dec 2025 09:18:18 -0300 Subject: [PATCH] nip05: use typed pubkeys. --- nip05/nip05.go | 15 +-- nip05/nip05_easyjson.go | 287 ++++++++++++++++++++++++++++++++++++++++ nip05/nip05_test.go | 35 ++++- 3 files changed, 323 insertions(+), 14 deletions(-) create mode 100644 nip05/nip05_easyjson.go diff --git a/nip05/nip05.go b/nip05/nip05.go index a4e1f99..727f0f0 100644 --- a/nip05/nip05.go +++ b/nip05/nip05.go @@ -14,9 +14,9 @@ import ( var NIP05_REGEX = regexp.MustCompile(`^(?:([\w.+-]+)@)?([\w_-]+(\.[\w_-]+)+)$`) type WellKnownResponse struct { - Names map[string]string `json:"names"` - Relays map[string][]string `json:"relays,omitempty"` - NIP46 map[string][]string `json:"nip46,omitempty"` + Names map[string]nostr.PubKey `json:"names"` + Relays map[nostr.PubKey][]string `json:"relays,omitempty"` + NIP46 map[nostr.PubKey][]string `json:"nip46,omitempty"` } func IsValidIdentifier(input string) bool { @@ -40,17 +40,12 @@ func QueryIdentifier(ctx context.Context, fullname string) (*nostr.ProfilePointe return nil, err } - pubkeyh, ok := result.Names[name] + pubkey, ok := result.Names[name] if !ok { return nil, fmt.Errorf("no entry for name '%s'", name) } - pubkey, err := nostr.PubKeyFromHex(pubkeyh) - if err != nil { - return nil, fmt.Errorf("got an invalid public key '%s'", pubkeyh) - } - - relays, _ := result.Relays[pubkeyh] + relays, _ := result.Relays[pubkey] return &nostr.ProfilePointer{ PublicKey: pubkey, Relays: relays, diff --git a/nip05/nip05_easyjson.go b/nip05/nip05_easyjson.go new file mode 100644 index 0000000..a94f592 --- /dev/null +++ b/nip05/nip05_easyjson.go @@ -0,0 +1,287 @@ +package nip05 + +import ( + json "encoding/json" + "errors" + "unsafe" + + nostr "fiatjaf.com/nostr" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjsonDecode(in *jlexer.Lexer, out *WellKnownResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "names": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + out.Names = make(map[string]nostr.PubKey) + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var pk nostr.PubKey + if data := in.Raw(); in.Ok() { + var err error + if len(data) < 2 { + err = errors.New("names[]{pubkey} must be a string") + } else { + data = data[1 : len(data)-1] + pk, err = nostr.PubKeyFromHex(unsafe.String(unsafe.SliceData(data), len(data))) + } + in.AddError(err) + } + out.Names[key] = pk + in.WantComma() + } + in.Delim('}') + } + case "relays": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.Relays = make(map[nostr.PubKey][]string) + } else { + out.Relays = nil + } + for !in.IsDelim('}') { + var key nostr.PubKey + if data := in.Raw(); in.Ok() { + var err error + if len(data) < 2 { + err = errors.New("relays[pubkey] must be a string") + } else { + data = data[1 : len(data)-1] + key, err = nostr.PubKeyFromHex(unsafe.String(unsafe.SliceData(data), len(data))) + } + in.AddError(err) + } + in.WantColon() + var relays []string + if in.IsNull() { + in.Skip() + relays = nil + } else { + in.Delim('[') + if relays == nil { + if !in.IsDelim(']') { + relays = make([]string, 0, 4) + } else { + relays = []string{} + } + } else { + relays = (relays)[:0] + } + for !in.IsDelim(']') { + relays = append(relays, string(in.String())) + in.WantComma() + } + in.Delim(']') + } + out.Relays[key] = relays + in.WantComma() + } + in.Delim('}') + } + case "nip46": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.NIP46 = make(map[nostr.PubKey][]string) + } else { + out.NIP46 = nil + } + for !in.IsDelim('}') { + var key nostr.PubKey + if data := in.Raw(); in.Ok() { + var err error + if len(data) < 2 { + err = errors.New("nip46[pubkey] must be a string") + } else { + data = data[1 : len(data)-1] + key, err = nostr.PubKeyFromHex(unsafe.String(unsafe.SliceData(data), len(data))) + } + in.AddError(err) + } + in.WantColon() + var bunkers []string + if in.IsNull() { + in.Skip() + bunkers = nil + } else { + in.Delim('[') + if bunkers == nil { + if !in.IsDelim(']') { + bunkers = make([]string, 0, 4) + } else { + bunkers = []string{} + } + } else { + bunkers = (bunkers)[:0] + } + for !in.IsDelim(']') { + var bunker string + bunker = string(in.String()) + bunkers = append(bunkers, bunker) + in.WantComma() + } + in.Delim(']') + } + out.NIP46[key] = bunkers + in.WantComma() + } + in.Delim('}') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} + +func easyjsonEncode(out *jwriter.Writer, in WellKnownResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"names\":" + out.RawString(prefix[1:]) + if in.Names == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { + out.RawString(`null`) + } else { + out.RawByte('{') + isFirst := true + for name, pk := range in.Names { + if isFirst { + isFirst = false + } else { + out.RawByte(',') + } + out.String(name) + out.RawByte(':') + out.String(pk.Hex()) + } + out.RawByte('}') + } + } + if len(in.Relays) != 0 { + const prefix string = ",\"relays\":" + out.RawString(prefix) + { + out.RawByte('{') + isFirst := true + for pk, relays := range in.Relays { + if isFirst { + isFirst = false + } else { + out.RawByte(',') + } + out.String(pk.Hex()) + out.RawByte(':') + if relays == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for i, relay := range relays { + if i > 0 { + out.RawByte(',') + } + out.String(relay) + } + out.RawByte(']') + } + } + out.RawByte('}') + } + } + if len(in.NIP46) != 0 { + const prefix string = ",\"nip46\":" + out.RawString(prefix) + { + out.RawByte('{') + isFirst := true + for pk, bunkers := range in.NIP46 { + if isFirst { + isFirst = false + } else { + out.RawByte(',') + } + out.String(pk.Hex()) + out.RawByte(':') + if bunkers == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for i, bunker := range bunkers { + if i > 0 { + out.RawByte(',') + } + out.String(bunker) + } + out.RawByte(']') + } + } + out.RawByte('}') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v WellKnownResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonEncode(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v WellKnownResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonEncode(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *WellKnownResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonDecode(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *WellKnownResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonDecode(l, v) +} diff --git a/nip05/nip05_test.go b/nip05/nip05_test.go index ebf55fc..6303a3e 100644 --- a/nip05/nip05_test.go +++ b/nip05/nip05_test.go @@ -2,10 +2,12 @@ package nip05 import ( "context" + "encoding/json" "testing" "fiatjaf.com/nostr" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestParse(t *testing.T) { @@ -38,11 +40,11 @@ func TestParse(t *testing.T) { func TestQuery(t *testing.T) { tests := []struct { input string - expectedKey string + expectedKey nostr.PubKey expectError bool }{ - {"fiatjaf.com", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", false}, - {"htlc@fiatjaf.com", "f9dd6a762506260b38a2d3e5b464213c2e47fa3877429fe9ee60e071a31a07d7", false}, + {"fiatjaf.com", nostr.MustPubKeyFromHex("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"), false}, + {"htlc@fiatjaf.com", nostr.MustPubKeyFromHex("f9dd6a762506260b38a2d3e5b464213c2e47fa3877429fe9ee60e071a31a07d7"), false}, } for _, test := range tests { @@ -51,7 +53,32 @@ func TestQuery(t *testing.T) { assert.Error(t, err, "expected error for input: %s", test.input) } else { assert.NoError(t, err, "did not expect error for input: %s", test.input) - assert.Equal(t, nostr.MustPubKeyFromHex(test.expectedKey), pp.PublicKey, "for input: %s", test.input) + assert.Equal(t, test.expectedKey, pp.PublicKey, "for input: %s", test.input) } } } + +func TestResponse(t *testing.T) { + pk1 := nostr.Generate().Public() + pk2 := nostr.Generate().Public() + + resp := WellKnownResponse{ + Names: map[string]nostr.PubKey{ + "foo": pk1, + "bar": pk2, + }, + Relays: map[nostr.PubKey][]string{ + pk1: {"wss://a.com"}, + pk2: {"wss://a.com", "wss://b.com"}, + }, + } + + respj, err := json.Marshal(resp) + require.NoError(t, err) + require.Equal(t, `{"names":{"foo":"`+pk1.Hex()+`","bar":"`+pk2.Hex()+`"},"relays":{"`+pk1.Hex()+`":["wss://a.com"],"`+pk2.Hex()+`":["wss://a.com","wss://b.com"]}}`, string(respj)) + + back := WellKnownResponse{} + err = json.Unmarshal(respj, &back) + require.NoError(t, err) + require.Equal(t, resp, back) +}