hex_utils.go raw
1 // Package neo4j provides hex utilities for normalizing pubkeys and event IDs.
2 //
3 // The nostr library applies binary optimization to e/p tags, storing 64-character
4 // hex strings as 33-byte binary (32 bytes + null terminator). This file provides
5 // utilities to ensure all pubkeys and event IDs stored in Neo4j are in consistent
6 // lowercase hex format.
7 package neo4j
8
9 import (
10 "strings"
11
12 "next.orly.dev/pkg/nostr/encoders/hex"
13 "next.orly.dev/pkg/nostr/encoders/tag"
14 )
15
16 // Tag binary encoding constants (matching the nostr library)
17 const (
18 // BinaryEncodedLen is the length of a binary-encoded 32-byte hash with null terminator
19 BinaryEncodedLen = 33
20 // HexEncodedLen is the length of a hex-encoded 32-byte hash (pubkey or event ID)
21 HexEncodedLen = 64
22 // HashLen is the raw length of a hash (pubkey/event ID)
23 HashLen = 32
24 )
25
26 // IsBinaryEncoded checks if a value is stored in the nostr library's binary-optimized format
27 func IsBinaryEncoded(val []byte) bool {
28 return len(val) == BinaryEncodedLen && val[HashLen] == 0
29 }
30
31 // NormalizePubkeyHex ensures a pubkey/event ID is in lowercase hex format.
32 // It handles:
33 // - Binary-encoded values (33 bytes with null terminator) -> converts to lowercase hex
34 // - Raw binary values (32 bytes) -> converts to lowercase hex
35 // - Uppercase hex strings -> converts to lowercase
36 // - Already lowercase hex -> returns as-is
37 //
38 // This should be used for all pubkeys and event IDs before storing in Neo4j
39 // to prevent duplicate nodes due to case differences.
40 func NormalizePubkeyHex(val []byte) string {
41 // Handle binary-encoded values from the nostr library (33 bytes with null terminator)
42 if IsBinaryEncoded(val) {
43 // Convert binary to lowercase hex
44 return hex.Enc(val[:HashLen])
45 }
46
47 // Handle raw binary values (32 bytes) - common when passing ev.ID or ev.Pubkey directly
48 if len(val) == HashLen {
49 // Convert binary to lowercase hex
50 return hex.Enc(val)
51 }
52
53 // Handle hex strings (may be uppercase from external sources)
54 if len(val) == HexEncodedLen {
55 return strings.ToLower(string(val))
56 }
57
58 // For other lengths (possibly prefixes), lowercase the hex
59 return strings.ToLower(string(val))
60 }
61
62 // ExtractPTagValue extracts a pubkey from a p-tag, handling binary encoding.
63 // Returns lowercase hex string suitable for Neo4j storage.
64 // Returns empty string if the tag doesn't have a valid value.
65 func ExtractPTagValue(t *tag.T) string {
66 if t == nil || len(t.T) < 2 {
67 return ""
68 }
69
70 // Use ValueHex() which properly handles both binary and hex formats
71 hexVal := t.ValueHex()
72 if len(hexVal) == 0 {
73 return ""
74 }
75
76 // Ensure lowercase (ValueHex returns the library's encoding which is lowercase,
77 // but we normalize anyway for safety with external data)
78 return strings.ToLower(string(hexVal))
79 }
80
81 // ExtractETagValue extracts an event ID from an e-tag, handling binary encoding.
82 // Returns lowercase hex string suitable for Neo4j storage.
83 // Returns empty string if the tag doesn't have a valid value.
84 func ExtractETagValue(t *tag.T) string {
85 if t == nil || len(t.T) < 2 {
86 return ""
87 }
88
89 // Use ValueHex() which properly handles both binary and hex formats
90 hexVal := t.ValueHex()
91 if len(hexVal) == 0 {
92 return ""
93 }
94
95 // Ensure lowercase
96 return strings.ToLower(string(hexVal))
97 }
98
99 // IsValidHexPubkey checks if a string is a valid 64-character hex pubkey
100 func IsValidHexPubkey(s string) bool {
101 if len(s) != HexEncodedLen {
102 return false
103 }
104 for _, c := range s {
105 if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
106 return false
107 }
108 }
109 return true
110 }
111