5 Commits

Author SHA1 Message Date
Jay
1e2a6f7777 Add license file. 2025-11-02 14:27:01 -05:00
Jay
d42d877ea2 Updated README. 2025-11-02 14:22:45 -05:00
Jay
4df91938ef Refactored methods into pure functions. 2025-10-31 19:48:56 -04:00
Jay
67db088981 Refactored into namespaced packages. 2025-10-31 19:12:21 -04:00
Jay
223c9faec0 Add test. 2025-10-27 17:40:46 -04:00
21 changed files with 218 additions and 164 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 nostr:npub10mtatsat7ph6rsq0w8u8npt8d86x4jfr2nqjnvld2439q6f8ugqq0x27hf
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -6,7 +6,7 @@ Mirror: https://github.com/wisehodl/go-roots
## What this library does
`go-roots` is a purposefully minimal Nostr protocol library for golang.
`go-roots` is a consensus-layer Nostr protocol library for golang.
It only provides primitives that define protocol compliance:
- Event Structure
@@ -31,13 +31,24 @@ mechanisms, and user interfaces.
go get git.wisehodl.dev/jay/go-roots
```
2. Import it with:
If the primary repository is unavailable, use the `replace` directive in your go.mod file to get the package from the github mirror:
```golang
import "git.wisehodl.dev/jay/go-roots"
```
replace git.wisehodl.dev/jay/go-roots => github.com/wisehodl/go-roots latest
```
3. Access it with the `roots` namespace.
2. Import the packages:
```golang
import (
"git.wisehodl.dev/jay/go-roots/errors"
"git.wisehodl.dev/jay/go-roots/events"
"git.wisehodl.dev/jay/go-roots/filters"
"git.wisehodl.dev/jay/go-roots/keys"
)
```
3. Access functions with appropriate namespaces.
## Usage Examples
@@ -46,12 +57,12 @@ import "git.wisehodl.dev/jay/go-roots"
#### Generate a new keypair
```go
privateKey, err := roots.GeneratePrivateKey()
privateKey, err := keys.GeneratePrivateKey()
if err != nil {
log.Fatal(err)
}
publicKey, err := roots.GetPublicKey(privateKey)
publicKey, err := keys.GetPublicKey(privateKey)
if err != nil {
log.Fatal(err)
}
@@ -61,7 +72,7 @@ if err != nil {
```go
privateKey := "f43a0435f69529f310bbd1d6263d2fbf0977f54bfe2310cc37ae5904b83bb167"
publicKey, err := roots.GetPublicKey(privateKey)
publicKey, err := keys.GetPublicKey(privateKey)
// publicKey: "cfa87f35acbde29ba1ab3ee42de527b2cad33ac487e80cf2d6405ea0042c8fef"
```
@@ -73,11 +84,11 @@ publicKey, err := roots.GetPublicKey(privateKey)
```go
// 1. Build the event structure
event := roots.Event{
event := events.Event{
PubKey: publicKey,
CreatedAt: int(time.Now().Unix()),
Kind: 1,
Tags: []roots.Tag{
Tags: []events.Tag{
{"e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"},
{"p", "91cf9b32f3735070f46c0a86a820a47efa08a5be6c9f4f8cf68e5b5b75c92d60"},
},
@@ -85,14 +96,14 @@ event := roots.Event{
}
// 2. Compute the event ID
id, err := event.GetID()
id, err := events.GetID(event)
if err != nil {
log.Fatal(err)
}
event.ID = id
// 3. Sign the event
sig, err := roots.SignEvent(id, privateKey)
sig, err := events.SignEvent(id, privateKey)
if err != nil {
log.Fatal(err)
}
@@ -103,7 +114,7 @@ event.Sig = sig
```go
// Returns canonical JSON: [0, pubkey, created_at, kind, tags, content]
serialized, err := event.Serialize()
serialized, err := events.Serialize(event)
if err != nil {
log.Fatal(err)
}
@@ -112,7 +123,7 @@ if err != nil {
#### Compute event ID manually
```go
id, err := event.GetID()
id, err := events.GetID(event)
if err != nil {
log.Fatal(err)
}
@@ -127,7 +138,7 @@ if err != nil {
```go
// Checks structure, ID computation, and signature
if err := event.Validate(); err != nil {
if err := events.Validate(event); err != nil {
log.Printf("Invalid event: %v", err)
}
```
@@ -136,17 +147,17 @@ if err := event.Validate(); err != nil {
```go
// Check field formats and lengths
if err := event.ValidateStructure(); err != nil {
if err := events.ValidateStructure(event); err != nil {
log.Printf("Malformed structure: %v", err)
}
// Verify ID matches computed hash
if err := event.ValidateID(); err != nil {
if err := events.ValidateID(event); err != nil {
log.Printf("ID mismatch: %v", err)
}
// Verify cryptographic signature
if err := event.ValidateSignature(); err != nil {
if err := events.ValidateSignature(event); err != nil {
log.Printf("Invalid signature: %v", err)
}
```
@@ -168,14 +179,14 @@ if err != nil {
#### Unmarshal event from JSON
```go
var event roots.Event
var event events.Event
err := json.Unmarshal(jsonBytes, &event)
if err != nil {
log.Fatal(err)
}
// Validate after unmarshaling
if err := event.Validate(); err != nil {
if err := events.Validate(event); err != nil {
log.Printf("Received invalid event: %v", err)
}
```
@@ -190,7 +201,7 @@ if err := event.Validate(); err != nil {
since := int(time.Now().Add(-24 * time.Hour).Unix())
limit := 50
filter := roots.Filter{
filter := filters.Filter{
IDs: []string{"abc123", "def456"}, // Prefix match
Authors: []string{"cfa87f35"}, // Prefix match
Kinds: []int{1, 6, 7},
@@ -202,9 +213,9 @@ filter := roots.Filter{
#### Filter with tag conditions
```go
filter := roots.Filter{
filter := filters.Filter{
Kinds: []int{1},
Tags: roots.TagFilters{
Tags: filters.TagFilters{
"e": {"5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"},
"p": {"91cf9b32f3735070f46c0a86a820a47efa08a5be6c9f4f8cf68e5b5b75c92d60"},
},
@@ -216,9 +227,9 @@ filter := roots.Filter{
```go
// Extensions allow arbitrary JSON fields beyond the standard filter spec.
// For example, this is how to implement non-standard filters like 'search'.
filter := roots.Filter{
filter := filters.Filter{
Kinds: []int{1},
Extensions: roots.FilterExtensions{
Extensions: filters.FilterExtensions{
"search": json.RawMessage(`"bitcoin"`),
},
}
@@ -234,12 +245,12 @@ filter := roots.Filter{
#### Match single event
```go
filter := roots.Filter{
filter := filters.Filter{
Authors: []string{"cfa87f35"},
Kinds: []int{1},
}
if filter.Matches(&event) {
if filters.Matches(filter, event) {
// Event satisfies all filter conditions
}
```
@@ -248,17 +259,17 @@ if filter.Matches(&event) {
```go
since := int(time.Now().Add(-1 * time.Hour).Unix())
filter := roots.Filter{
filter := filters.Filter{
Kinds: []int{1},
Since: &since,
Tags: roots.TagFilters{
Tags: filters.TagFilters{
"p": {"abc123", "def456"}, // OR within tag values
},
}
var matches []roots.Event
var matches []events.Event
for _, event := range events {
if filter.Matches(&event) {
if filters.Matches(filter, event) {
matches = append(matches, event)
}
}
@@ -271,18 +282,18 @@ for _, event := range events {
#### Marshal filter to JSON
```go
filter := roots.Filter{
filter := filters.Filter{
IDs: []string{"abc123"},
Kinds: []int{1},
Tags: roots.TagFilters{
Tags: filters.TagFilters{
"e": {"event-id"},
},
Extensions: roots.FilterExtensions{
Extensions: filters.FilterExtensions{
"search": json.RawMessage(`"nostr"`),
},
}
jsonBytes, err := filter.MarshalJSON()
jsonBytes, err := filters.MarshalJSON(filter)
// Result: {"ids":["abc123"],"kinds":[1],"#e":["event-id"],"search":"nostr"}
```
@@ -297,8 +308,8 @@ jsonData := `{
"search": "bitcoin"
}`
var filter roots.Filter
err := filter.UnmarshalJSON([]byte(jsonData))
var filter filters.Filter
err := filters.UnmarshalJSON([]byte(jsonData), &filter)
if err != nil {
log.Fatal(err)
}
@@ -323,9 +334,9 @@ During marshaling, Extensions merge into the output JSON. During unmarshaling, u
Example implementing search filter:
```go
filter := roots.Filter{
filter := filters.Filter{
Kinds: []int{1},
Extensions: roots.FilterExtensions{
Extensions: filters.FilterExtensions{
"search": json.RawMessage(`"bitcoin"`),
},
}
@@ -343,5 +354,5 @@ if searchRaw, ok := filter.Extensions["search"]; ok {
This library contains a comprehensive suite of unit tests. Run them with:
```bash
go test
go test ./...
```

31
errors/errors.go Normal file
View File

@@ -0,0 +1,31 @@
package errors
import (
"errors"
)
var (
// MalformedPubKey indicates a public key is not 64 lowercase hex characters.
MalformedPubKey = errors.New("public key must be 64 lowercase hex characters")
// MalformedPrivKey indicates a private key is not 64 lowercase hex characters.
MalformedPrivKey = errors.New("private key must be 64 lowercase hex characters")
// MalformedID indicates an event id is not 64 hex characters.
MalformedID = errors.New("event id must be 64 hex characters")
// MalformedSig indicates an event signature is not 128 hex characters.
MalformedSig = errors.New("event signature must be 128 hex characters")
// MalformedTag indicates an event tag contains fewer than two elements.
MalformedTag = errors.New("tags must contain at least two elements")
// FailedIDComp indicates the event ID could not be computed during validation.
FailedIDComp = errors.New("failed to compute event id")
// NoEventID indicates the event ID field is empty.
NoEventID = errors.New("event id is empty")
// InvalidSig indicates the event signature failed cryptographic validation.
InvalidSig = errors.New("event signature is invalid")
)

View File

@@ -1,62 +0,0 @@
// Roots is a purposefully minimal Nostr protocol library that provides only
// the primitives that define protocol compliance: event structure,
// serialization, cryptographic signatures, and subscription filters.
package roots
import (
"errors"
"regexp"
)
// Tag represents a single tag within an event as an array of strings.
// The first element identifies the tag name, the second contains the value,
// and subsequent elements are optional.
type Tag []string
// Event represents a Nostr protocol event, with its seven required fields.
// All fields must be present for a valid event.
type Event struct {
ID string `json:"id"`
PubKey string `json:"pubkey"`
CreatedAt int `json:"created_at"`
Kind int `json:"kind"`
Tags []Tag `json:"tags"`
Content string `json:"content"`
Sig string `json:"sig"`
}
var (
// Hex64Pattern matches 64-character, lowercase, hexadecimal strings.
// Used for validating event IDs and cryptographic keys.
Hex64Pattern = regexp.MustCompile("^[a-f0-9]{64}$")
// Hex128Pattern matches 128-character, lowercase, hexadecimal strings.
// Used for validating signatures.
Hex128Pattern = regexp.MustCompile("^[a-f0-9]{128}$")
)
var (
// ErrMalformedPubKey indicates a public key is not 64 lowercase hex characters.
ErrMalformedPubKey = errors.New("public key must be 64 lowercase hex characters")
// ErrMalformedPrivKey indicates a private key is not 64 lowercase hex characters.
ErrMalformedPrivKey = errors.New("private key must be 64 lowercase hex characters")
// ErrMalformedID indicates an event id is not 64 hex characters.
ErrMalformedID = errors.New("event id must be 64 hex characters")
// ErrMalformedSig indicates an event signature is not 128 hex characters.
ErrMalformedSig = errors.New("event signature must be 128 hex characters")
// ErrMalformedTag indicates an event tag contains fewer than two elements.
ErrMalformedTag = errors.New("tags must contain at least two elements")
// ErrFailedIDComp indicates the event ID could not be computed during validation.
ErrFailedIDComp = errors.New("failed to compute event id")
// ErrNoEventID indicates the event ID field is empty.
ErrNoEventID = errors.New("event id is empty")
// ErrInvalidSig indicates the event signature failed cryptographic validation.
ErrInvalidSig = errors.New("event signature is invalid")
)

35
events/event.go Normal file
View File

@@ -0,0 +1,35 @@
// Roots is a purposefully minimal Nostr protocol library that provides only
// the primitives that define protocol compliance: event structure,
// serialization, cryptographic signatures, and subscription filters.
package events
import (
"regexp"
)
// Tag represents a single tag within an event as an array of strings.
// The first element identifies the tag name, the second contains the value,
// and subsequent elements are optional.
type Tag []string
// Event represents a Nostr protocol event, with its seven required fields.
// All fields must be present for a valid event.
type Event struct {
ID string `json:"id"`
PubKey string `json:"pubkey"`
CreatedAt int `json:"created_at"`
Kind int `json:"kind"`
Tags []Tag `json:"tags"`
Content string `json:"content"`
Sig string `json:"sig"`
}
var (
// Hex64Pattern matches 64-character, lowercase, hexadecimal strings.
// Used for validating event IDs and cryptographic keys.
Hex64Pattern = regexp.MustCompile("^[a-f0-9]{64}$")
// Hex128Pattern matches 128-character, lowercase, hexadecimal strings.
// Used for validating signatures.
Hex128Pattern = regexp.MustCompile("^[a-f0-9]{128}$")
)

View File

@@ -1,4 +1,4 @@
package roots
package events
import (
"encoding/json"
@@ -9,7 +9,7 @@ import (
func TestUnmarshalEventJSON(t *testing.T) {
event := Event{}
json.Unmarshal(testEventJSONBytes, &event)
if err := event.Validate(); err != nil {
if err := Validate(event); err != nil {
t.Error("unmarshalled event is invalid")
}
expectEqualEvents(t, event, testEvent)
@@ -37,7 +37,7 @@ func TestEventJSONRoundTrip(t *testing.T) {
}
expectedJSON := `{"id":"86e856d0527dd08527498cd8afd8a7d296bde37e4757a8921f034f0b344df3ad","pubkey":"cfa87f35acbde29ba1ab3ee42de527b2cad33ac487e80cf2d6405ea0042c8fef","created_at":1760740551,"kind":1,"tags":[["a","value"],["b","value","optional"],["name","value","optional","optional"]],"content":"hello world","sig":"c05fe02a9c082ff56aad2b16b5347498a21665f02f050ba086dbe6bd593c8cd448505d2831d1c0340acc1793eaf89b7c0cb21bb696c71da6b8d6b857702bb557"}`
if err := event.Validate(); err != nil {
if err := Validate(event); err != nil {
t.Error("test event is invalid")
}
eventJSON, err := json.Marshal(event)
@@ -47,7 +47,7 @@ func TestEventJSONRoundTrip(t *testing.T) {
unmarshalledEvent := Event{}
json.Unmarshal(eventJSON, &unmarshalledEvent)
if err := unmarshalledEvent.Validate(); err != nil {
if err := Validate(unmarshalledEvent); err != nil {
t.Error("unmarshalled event is invalid")
}
expectEqualEvents(t, unmarshalledEvent, event)

View File

@@ -1,4 +1,4 @@
package roots
package events
import (
"github.com/stretchr/testify/assert"

View File

@@ -1,4 +1,4 @@
package roots
package events
import (
"crypto/sha256"
@@ -8,7 +8,7 @@ import (
// Serialize returns the canonical JSON array representation of the event.
// used for ID computation: [0, pubkey, created_at, kind, tags, content].
func (e *Event) Serialize() ([]byte, error) {
func Serialize(e Event) ([]byte, error) {
serialized := []interface{}{
0,
e.PubKey,
@@ -27,8 +27,8 @@ func (e *Event) Serialize() ([]byte, error) {
// GetID computes and returns the event ID as a lowercase, hex-encoded SHA-256 hash
// of the serialized event.
func (e *Event) GetID() (string, error) {
bytes, err := e.Serialize()
func GetID(e Event) (string, error) {
bytes, err := Serialize(e)
if err != nil {
return "", err
}

View File

@@ -1,4 +1,4 @@
package roots
package events
import (
"github.com/stretchr/testify/assert"
@@ -196,7 +196,7 @@ var idTestCases = []IDTestCase{
func TestEventGetId(t *testing.T) {
for _, tc := range idTestCases {
t.Run(tc.name, func(t *testing.T) {
actual, err := tc.event.GetID()
actual, err := GetID(tc.event)
assert.NoError(t, err)
assert.Equal(t, tc.expected, actual)
})

View File

@@ -1,8 +1,9 @@
package roots
package events
import (
"encoding/hex"
"fmt"
"git.wisehodl.dev/jay/go-roots/errors"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
)
@@ -12,12 +13,12 @@ import (
func SignEvent(eventID, privateKeyHex string) (string, error) {
skBytes, err := hex.DecodeString(privateKeyHex)
if err != nil {
return "", ErrMalformedPrivKey
return "", errors.MalformedPrivKey
}
idBytes, err := hex.DecodeString(eventID)
if err != nil {
return "", ErrMalformedID
return "", errors.MalformedID
}
// discard public key return value

View File

@@ -1,4 +1,4 @@
package roots
package events
import (
"github.com/stretchr/testify/assert"

View File

@@ -1,4 +1,4 @@
package roots
package events
func intPtr(i int) *int {
return &i

View File

@@ -1,44 +1,44 @@
package roots
package events
import (
"encoding/hex"
"fmt"
"git.wisehodl.dev/jay/go-roots/errors"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
)
// Validate performs a complete event validation: structure, ID computation,
// and signature verification. Returns the first error encountered.
func (e *Event) Validate() error {
if err := e.ValidateStructure(); err != nil {
func Validate(e Event) error {
if err := ValidateStructure(e); err != nil {
return err
}
if err := e.ValidateID(); err != nil {
if err := ValidateID(e); err != nil {
return err
}
return e.ValidateSignature()
return ValidateSignature(e)
}
// ValidateStructure checks that all event fields conform to the protocol
// specification: hex lengths, tag structure, and field formats.
func (e *Event) ValidateStructure() error {
func ValidateStructure(e Event) error {
if !Hex64Pattern.MatchString(e.PubKey) {
return ErrMalformedPubKey
return errors.MalformedPubKey
}
if !Hex64Pattern.MatchString(e.ID) {
return ErrMalformedID
return errors.MalformedID
}
if !Hex128Pattern.MatchString(e.Sig) {
return ErrMalformedSig
return errors.MalformedSig
}
for _, tag := range e.Tags {
if len(tag) < 2 {
return ErrMalformedTag
return errors.MalformedTag
}
}
@@ -46,13 +46,13 @@ func (e *Event) ValidateStructure() error {
}
// ValidateID recomputes the event ID and verifies it matches the stored ID field.
func (e *Event) ValidateID() error {
computedID, err := e.GetID()
func ValidateID(e Event) error {
computedID, err := GetID(e)
if err != nil {
return ErrFailedIDComp
return errors.FailedIDComp
}
if e.ID == "" {
return ErrNoEventID
return errors.NoEventID
}
if computedID != e.ID {
return fmt.Errorf("event id %q does not match computed id %q", e.ID, computedID)
@@ -62,7 +62,7 @@ func (e *Event) ValidateID() error {
// ValidateSignature verifies the event signature is cryptographically valid
// for the event ID and public key using Schnorr verification.
func (e *Event) ValidateSignature() error {
func ValidateSignature(e Event) error {
idBytes, err := hex.DecodeString(e.ID)
if err != nil {
return fmt.Errorf("invalid event id hex: %w", err)
@@ -91,6 +91,6 @@ func (e *Event) ValidateSignature() error {
if signature.Verify(idBytes, publicKey) {
return nil
} else {
return ErrInvalidSig
return errors.InvalidSig
}
}

View File

@@ -1,4 +1,4 @@
package roots
package events
import (
"github.com/stretchr/testify/assert"
@@ -184,7 +184,7 @@ var structureTestCases = []ValidateEventTestCase{
func TestValidateEventStructure(t *testing.T) {
for _, tc := range structureTestCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.event.ValidateStructure()
err := ValidateStructure(tc.event)
assert.ErrorContains(t, err, tc.expectedError)
})
}
@@ -201,7 +201,7 @@ func TestValidateEventIDFailure(t *testing.T) {
Sig: testEvent.Sig,
}
err := event.ValidateID()
err := ValidateID(event)
assert.ErrorContains(t, err, "does not match computed id")
}
@@ -211,7 +211,7 @@ func TestValidateSignature(t *testing.T) {
PubKey: testEvent.PubKey,
Sig: testEvent.Sig,
}
err := event.ValidateSignature()
err := ValidateSignature(event)
assert.NoError(t, err)
}
@@ -222,7 +222,7 @@ func TestValidateInvalidSignature(t *testing.T) {
PubKey: testEvent.PubKey,
Sig: "9e43cbcf7e828a21c53fa35371ee79bffbfd7a3063ae46fc05ec623dd3186667c57e3d006488015e19247df35eb41c61013e051aa87860e23fa5ffbd44120482",
}
err := event.ValidateSignature()
err := ValidateSignature(event)
assert.ErrorContains(t, err, "event signature is invalid")
}
@@ -281,7 +281,7 @@ func TestValidateSignatureInvalidEventSignature(t *testing.T) {
for _, tc := range validateSignatureTestCases {
t.Run(tc.name, func(t *testing.T) {
event := Event{ID: tc.id, PubKey: tc.pubkey, Sig: tc.sig}
err := event.ValidateSignature()
err := ValidateSignature(event)
assert.ErrorContains(t, err, tc.expectedError)
})
}
@@ -301,6 +301,6 @@ func TestValidateEvent(t *testing.T) {
Sig: "668a715f1eb983172acf230d17bd283daedb2598adf8de4290bcc7eb0b802fdb60669d1e7d1104ac70393f4dbccd07e8abf897152af6ce6c0a75499874e27f14",
}
err := event.Validate()
err := Validate(event)
assert.NoError(t, err)
}

View File

@@ -1,7 +1,8 @@
package roots
package filters
import (
"encoding/json"
"git.wisehodl.dev/jay/go-roots/events"
"strings"
)
@@ -28,7 +29,7 @@ type Filter struct {
// MarshalJSON converts the filter to JSON with standard fields, tag filters
// (prefixed with "#"), and extensions merged into a single object.
func (f *Filter) MarshalJSON() ([]byte, error) {
func MarshalJSON(f Filter) ([]byte, error) {
outputMap := make(map[string]interface{})
// Add standard fields
@@ -85,7 +86,7 @@ func (f *Filter) MarshalJSON() ([]byte, error) {
// UnmarshalJSON parses JSON into the filter, separating standard fields,
// tag filters (keys starting with "#"), and extensions.
func (f *Filter) UnmarshalJSON(data []byte) error {
func UnmarshalJSON(data []byte, f *Filter) error {
// Decode into raw map
raw := make(FilterExtensions)
if err := json.Unmarshal(data, &raw); err != nil {
@@ -181,7 +182,7 @@ func (f *Filter) UnmarshalJSON(data []byte) error {
// Matches returns true if the event satisfies all filter conditions.
// Supports prefix matching for IDs and authors, and tag filtering.
// Does not account for custom extensions.
func (f *Filter) Matches(event *Event) bool {
func Matches(f Filter, event events.Event) bool {
// Check ID
if len(f.IDs) > 0 {
if !matchesPrefix(event.ID, f.IDs) {
@@ -246,7 +247,7 @@ func matchesTimeRange(timestamp int, since *int, until *int) bool {
return true
}
func matchesTags(eventTags []Tag, tagFilters *TagFilters) bool {
func matchesTags(eventTags []events.Tag, tagFilters *TagFilters) bool {
// Build index of tags and values
eventIndex := make(map[string][]string, len(eventTags))
for _, tag := range eventTags {

View File

@@ -1,4 +1,4 @@
package roots
package filters
import (
"encoding/json"
@@ -584,7 +584,7 @@ var roundTripTestCases = []FilterRoundTripTestCase{
func TestFilterMarshalJSON(t *testing.T) {
for _, tc := range marshalTestCases {
t.Run(tc.name, func(t *testing.T) {
result, err := tc.filter.MarshalJSON()
result, err := MarshalJSON(tc.filter)
assert.NoError(t, err)
var expectedMap, actualMap map[string]interface{}
@@ -602,7 +602,7 @@ func TestFilterUnmarshalJSON(t *testing.T) {
for _, tc := range unmarshalTestCases {
t.Run(tc.name, func(t *testing.T) {
var result Filter
err := result.UnmarshalJSON([]byte(tc.input))
err := UnmarshalJSON([]byte(tc.input), &result)
assert.NoError(t, err)
expectEqualFilters(t, result, tc.expected)
@@ -613,11 +613,11 @@ func TestFilterUnmarshalJSON(t *testing.T) {
func TestFilterRoundTrip(t *testing.T) {
for _, tc := range roundTripTestCases {
t.Run(tc.name, func(t *testing.T) {
jsonBytes, err := tc.filter.MarshalJSON()
jsonBytes, err := MarshalJSON(tc.filter)
assert.NoError(t, err)
var result Filter
err = result.UnmarshalJSON(jsonBytes)
err = UnmarshalJSON(jsonBytes, &result)
assert.NoError(t, err)
expectEqualFilters(t, result, tc.filter)

View File

@@ -1,13 +1,14 @@
package roots
package filters
import (
"encoding/json"
"git.wisehodl.dev/jay/go-roots/events"
"github.com/stretchr/testify/assert"
"os"
"testing"
)
var testEvents []Event
var testEvents []events.Event
func init() {
data, err := os.ReadFile("testdata/test_events.json")
@@ -390,7 +391,7 @@ func TestEventFilterMatching(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
actualIDs := []string{}
for _, event := range testEvents {
if tc.filter.Matches(&event) {
if Matches(tc.filter, event) {
actualIDs = append(actualIDs, event.ID[:8])
}
}
@@ -403,8 +404,8 @@ func TestEventFilterMatching(t *testing.T) {
// TestEventFilterMatchingSkipMalformedTags documents that filter.Matches()
// skips malformed tags during tag matching
func TestEventFilterMatchingSkipMalformedTags(t *testing.T) {
event := Event{
Tags: []Tag{
event := events.Event{
Tags: []events.Tag{
{"malformed"},
{"valid", "value"},
},
@@ -415,5 +416,5 @@ func TestEventFilterMatchingSkipMalformedTags(t *testing.T) {
},
}
assert.True(t, filter.Matches(&event))
assert.True(t, Matches(filter, event))
}

5
filters/util_test.go Normal file
View File

@@ -0,0 +1,5 @@
package filters
func intPtr(i int) *int {
return &i
}

View File

@@ -1,7 +1,8 @@
package roots
package keys
import (
"encoding/hex"
"git.wisehodl.dev/jay/go-roots/errors"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
@@ -20,11 +21,11 @@ func GeneratePrivateKey() (string, error) {
// and returns the x-coordinate as 64 lowercase hex characters.
func GetPublicKey(privateKeyHex string) (string, error) {
if len(privateKeyHex) != 64 {
return "", ErrMalformedPrivKey
return "", errors.MalformedPrivKey
}
skBytes, err := hex.DecodeString(privateKeyHex)
if err != nil {
return "", ErrMalformedPrivKey
return "", errors.MalformedPrivKey
}
pk := secp256k1.PrivKeyFromBytes(skBytes).PubKey()

View File

@@ -1,4 +1,4 @@
package roots
package keys
import (
"github.com/stretchr/testify/assert"
@@ -6,17 +6,26 @@ import (
"testing"
)
var hexPattern = regexp.MustCompile("^[a-f0-9]{64}$")
const testSK = "f43a0435f69529f310bbd1d6263d2fbf0977f54bfe2310cc37ae5904b83bb167"
const testPK = "cfa87f35acbde29ba1ab3ee42de527b2cad33ac487e80cf2d6405ea0042c8fef"
var Hex64Pattern = regexp.MustCompile("^[a-f0-9]{64}$")
func TestGeneratePrivateKey(t *testing.T) {
sk, err := GeneratePrivateKey()
assert.NoError(t, err)
if !hexPattern.MatchString(sk) {
if !Hex64Pattern.MatchString(sk) {
t.Errorf("invalid private key format: %s", sk)
}
}
func TestGenerateUniquePrivateKeys(t *testing.T) {
sk1, _ := GeneratePrivateKey()
sk2, _ := GeneratePrivateKey()
assert.NotEqual(t, sk1, sk2)
}
func TestGetPublicKey(t *testing.T) {
pk, err := GetPublicKey(testSK)