From e4c5dfbebb306816f620d1ebdfbdc008765c1e8c Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 27 Dec 2022 07:49:26 -0300 Subject: [PATCH] nip19. --- nip19/nip19.go | 140 ++++++++++++++++++++++++++++++++++++++++---- nip19/nip19_test.go | 86 +++++++++++++++++++++++++++ nip19/utils.go | 29 ++++++--- 3 files changed, 234 insertions(+), 21 deletions(-) create mode 100644 nip19/nip19_test.go diff --git a/nip19/nip19.go b/nip19/nip19.go index e49950c..f45127e 100644 --- a/nip19/nip19.go +++ b/nip19/nip19.go @@ -1,17 +1,106 @@ package nip19 import ( + "bytes" "encoding/hex" "fmt" ) +type ProfilePointer struct { + PublicKey string + Relays []string +} + +type EventPointer struct { + ID string + Relays []string +} + +func Decode(bech32string string) (prefix string, value any, err error) { + prefix, bits5, err := decode(bech32string) + if err != nil { + return "", nil, err + } + + data, err := convertBits(bits5, 5, 8, false) + if err != nil { + return prefix, nil, fmt.Errorf("failed translating data into 8 bits: %s", err.Error()) + } + + switch prefix { + case "npub", "nsec", "note": + if len(data) < 32 { + return prefix, nil, fmt.Errorf("data is less than 32 bytes (%d)", len(data)) + } + + return prefix, hex.EncodeToString(data[0:32]), nil + case "nprofile": + var result ProfilePointer + curr := 0 + for { + t, v := readTLVEntry(data[curr:]) + if v == nil { + // end here + if result.PublicKey == "" { + return prefix, result, fmt.Errorf("no pubkey found for nprofile") + } + + return prefix, result, nil + } + + switch t { + case TLVDefault: + result.PublicKey = hex.EncodeToString(v) + case TLVRelay: + result.Relays = append(result.Relays, string(v)) + default: + // ignore + } + + curr = curr + 2 + len(v) + } + case "nevent": + var result EventPointer + curr := 0 + for { + t, v := readTLVEntry(data[curr:]) + if v == nil { + // end here + if result.ID == "" { + return prefix, result, fmt.Errorf("no id found for nevent") + } + + return prefix, result, nil + } + + switch t { + case TLVDefault: + result.ID = hex.EncodeToString(v) + case TLVRelay: + result.Relays = append(result.Relays, string(v)) + default: + // ignore + } + + curr = curr + 2 + len(v) + } + } + + return prefix, data, fmt.Errorf("unknown tag %s", prefix) +} + func EncodePrivateKey(privateKeyHex string) (string, error) { b, err := hex.DecodeString(privateKeyHex) + if err != nil { + return "", fmt.Errorf("failed to decode private key hex: %w", err) + } + + bits5, err := convertBits(b, 8, 5, true) if err != nil { return "", err } - return encode("nsec", b) + return encode("nsec", bits5) } func EncodePublicKey(publicKeyHex string) (string, error) { @@ -30,27 +119,54 @@ func EncodePublicKey(publicKeyHex string) (string, error) { func EncodeNote(eventIdHex string) (string, error) { b, err := hex.DecodeString(eventIdHex) + if err != nil { + return "", fmt.Errorf("failed to decode event id hex: %w", err) + } + + bits5, err := convertBits(b, 8, 5, true) if err != nil { return "", err } - return encode("note", b) + return encode("note", bits5) } -func Decode(bech32string string) ([]byte, string, error) { - prefix, data, err := decode(bech32string) +func EncodeProfile(publicKeyHex string, relays []string) (string, error) { + buf := &bytes.Buffer{} + pubkey, err := hex.DecodeString(publicKeyHex) if err != nil { - return nil, "", err + return "", fmt.Errorf("invalid pubkey '%s': %w", publicKeyHex, err) + } + writeTLVEntry(buf, TLVDefault, pubkey) + + for _, url := range relays { + writeTLVEntry(buf, TLVRelay, []byte(url)) } - bits8, err := convertBits(data, 5, 8, false) + bits5, err := convertBits(buf.Bytes(), 8, 5, true) if err != nil { - return nil, "", fmt.Errorf("failed translating data into 8 bits: %s", err.Error()) + return "", fmt.Errorf("failed to convert bits: %w", err) } - if len(data) < 32 { - return nil, "", fmt.Errorf("data is less than 32 bytes (%d)", len(data)) - } - - return bits8[0:32], prefix, nil + return encode("nprofile", bits5) +} + +func EncodeEvent(eventIdHex string, relays []string) (string, error) { + buf := &bytes.Buffer{} + pubkey, err := hex.DecodeString(eventIdHex) + if err != nil { + return "", fmt.Errorf("invalid id '%s': %w", eventIdHex, err) + } + writeTLVEntry(buf, TLVDefault, pubkey) + + for _, url := range relays { + writeTLVEntry(buf, TLVRelay, []byte(url)) + } + + bits5, err := convertBits(buf.Bytes(), 8, 5, true) + if err != nil { + return "", fmt.Errorf("failed to convert bits: %w", err) + } + + return encode("nevent", bits5) } diff --git a/nip19/nip19_test.go b/nip19/nip19_test.go new file mode 100644 index 0000000..ed27a1a --- /dev/null +++ b/nip19/nip19_test.go @@ -0,0 +1,86 @@ +package nip19 + +import ( + "fmt" + "testing" +) + +func TestEncodeNpub(t *testing.T) { + npub, err := EncodePublicKey("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d") + if err != nil { + t.Errorf("shouldn't error: %s", err) + } + if npub != "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6" { + t.Error("produced an unexpected npub string") + } +} + +func TestEncodeNsec(t *testing.T) { + npub, err := EncodePrivateKey("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d") + if err != nil { + t.Errorf("shouldn't error: %s", err) + } + if npub != "nsec180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsgyumg0" { + t.Error("produced an unexpected nsec string") + } +} + +func TestDecodeNpub(t *testing.T) { + prefix, pubkey, err := Decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") + if err != nil { + t.Errorf("shouldn't error: %s", err) + } + if prefix != "npub" { + t.Error("returned invalid prefix") + } + if pubkey.(string) != "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" { + t.Error("returned wrong pubkey") + } +} + +func TestFailDecodeBadChecksumNpub(t *testing.T) { + _, _, err := Decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w4") + if err == nil { + t.Errorf("should have errored: %s", err) + } +} + +func TestDecodeNprofile(t *testing.T) { + prefix, data, err := Decode("nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p") + if err != nil { + t.Error("failed to decode nprofile") + } + if prefix != "nprofile" { + t.Error("what") + } + pp, ok := data.(ProfilePointer) + if !ok { + t.Error("value returned of wrong type") + } + + fmt.Println(pp) + + if pp.PublicKey != "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" { + t.Error("decoded invalid public key") + } + + if len(pp.Relays) != 2 { + t.Error("decoded wrong number of relays") + } + if pp.Relays[0] != "wss://r.x.com" || pp.Relays[1] != "wss://djbas.sadkb.com" { + t.Error("decoded relay URLs wrongly") + } +} + +func TestEncodeNprofile(t *testing.T) { + nprofile, err := EncodeProfile("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", []string{ + "wss://r.x.com", + "wss://djbas.sadkb.com", + }) + if err != nil { + t.Errorf("shouldn't error: %s", err) + } + if nprofile != "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p" { + t.Error("produced an unexpected nprofile string") + } +} diff --git a/nip19/utils.go b/nip19/utils.go index f475b12..517e334 100644 --- a/nip19/utils.go +++ b/nip19/utils.go @@ -1,17 +1,28 @@ package nip19 import ( - "encoding/hex" - "strings" + "bytes" ) -// TranslatePublicKey turns a hex or bech32 public key into always hex -func TranslatePublicKey(bech32orHexKey string) string { - if strings.HasPrefix(bech32orHexKey, "npub1") { - data, _, _ := Decode(bech32orHexKey) - return hex.EncodeToString(data) +const ( + TLVDefault uint8 = 0 + TLVRelay uint8 = 1 +) + +func readTLVEntry(data []byte) (typ uint8, value []byte) { + if len(data) < 2 { + return 0, nil } - // just return what we got - return bech32orHexKey + typ = data[0] + length := int(data[1]) + value = data[2 : 2+length] + return +} + +func writeTLVEntry(buf *bytes.Buffer, typ uint8, value []byte) { + length := len(value) + buf.WriteByte(typ) + buf.WriteByte(uint8(length)) + buf.Write(value) }