nip05: use typed pubkeys.

This commit is contained in:
fiatjaf
2025-12-14 09:18:18 -03:00
parent 658a40e16c
commit 1176d12b0a
3 changed files with 323 additions and 14 deletions

View File

@@ -14,9 +14,9 @@ import (
var NIP05_REGEX = regexp.MustCompile(`^(?:([\w.+-]+)@)?([\w_-]+(\.[\w_-]+)+)$`) var NIP05_REGEX = regexp.MustCompile(`^(?:([\w.+-]+)@)?([\w_-]+(\.[\w_-]+)+)$`)
type WellKnownResponse struct { type WellKnownResponse struct {
Names map[string]string `json:"names"` Names map[string]nostr.PubKey `json:"names"`
Relays map[string][]string `json:"relays,omitempty"` Relays map[nostr.PubKey][]string `json:"relays,omitempty"`
NIP46 map[string][]string `json:"nip46,omitempty"` NIP46 map[nostr.PubKey][]string `json:"nip46,omitempty"`
} }
func IsValidIdentifier(input string) bool { func IsValidIdentifier(input string) bool {
@@ -40,17 +40,12 @@ func QueryIdentifier(ctx context.Context, fullname string) (*nostr.ProfilePointe
return nil, err return nil, err
} }
pubkeyh, ok := result.Names[name] pubkey, ok := result.Names[name]
if !ok { if !ok {
return nil, fmt.Errorf("no entry for name '%s'", name) return nil, fmt.Errorf("no entry for name '%s'", name)
} }
pubkey, err := nostr.PubKeyFromHex(pubkeyh) relays, _ := result.Relays[pubkey]
if err != nil {
return nil, fmt.Errorf("got an invalid public key '%s'", pubkeyh)
}
relays, _ := result.Relays[pubkeyh]
return &nostr.ProfilePointer{ return &nostr.ProfilePointer{
PublicKey: pubkey, PublicKey: pubkey,
Relays: relays, Relays: relays,

287
nip05/nip05_easyjson.go Normal file
View File

@@ -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)
}

View File

@@ -2,10 +2,12 @@ package nip05
import ( import (
"context" "context"
"encoding/json"
"testing" "testing"
"fiatjaf.com/nostr" "fiatjaf.com/nostr"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
@@ -38,11 +40,11 @@ func TestParse(t *testing.T) {
func TestQuery(t *testing.T) { func TestQuery(t *testing.T) {
tests := []struct { tests := []struct {
input string input string
expectedKey string expectedKey nostr.PubKey
expectError bool expectError bool
}{ }{
{"fiatjaf.com", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", false}, {"fiatjaf.com", nostr.MustPubKeyFromHex("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"), false},
{"htlc@fiatjaf.com", "f9dd6a762506260b38a2d3e5b464213c2e47fa3877429fe9ee60e071a31a07d7", false}, {"htlc@fiatjaf.com", nostr.MustPubKeyFromHex("f9dd6a762506260b38a2d3e5b464213c2e47fa3877429fe9ee60e071a31a07d7"), false},
} }
for _, test := range tests { for _, test := range tests {
@@ -51,7 +53,32 @@ func TestQuery(t *testing.T) {
assert.Error(t, err, "expected error for input: %s", test.input) assert.Error(t, err, "expected error for input: %s", test.input)
} else { } else {
assert.NoError(t, err, "did not expect error for input: %s", test.input) 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)
}