Browse Source
- Add nurl: NIP-98 authenticated HTTP client for testing owner APIs - Add vainstr: vanity npub generator using fast secp256k1 library - Update CLAUDE.md with documentation for both tools - Properly handle secp256k1 library loading via p8k.New() Files modified: - cmd/nurl/main.go: New NIP-98 HTTP client tool - cmd/vainstr/main.go: New vanity npub generator - CLAUDE.md: Added usage documentation for nurl and vainstr - go.mod/go.sum: Added go-arg dependency for vainstr - pkg/version/version: Bump to v0.39.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>main
6 changed files with 455 additions and 1 deletions
@ -0,0 +1,201 @@
@@ -0,0 +1,201 @@
|
||||
// Package main is a simple implementation of a cURL like tool that can do
|
||||
// simple GET/POST operations on a HTTP server that understands NIP-98
|
||||
// authentication, with the signing key found in an environment variable.
|
||||
package main |
||||
|
||||
import ( |
||||
"crypto/sha256" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"net/url" |
||||
"os" |
||||
"strings" |
||||
|
||||
"lol.mleku.dev/chk" |
||||
"lol.mleku.dev/log" |
||||
|
||||
"git.mleku.dev/mleku/nostr/encoders/bech32encoding" |
||||
"git.mleku.dev/mleku/nostr/encoders/hex" |
||||
"git.mleku.dev/mleku/nostr/httpauth" |
||||
"git.mleku.dev/mleku/nostr/interfaces/signer" |
||||
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k" |
||||
|
||||
"next.orly.dev/pkg/version" |
||||
) |
||||
|
||||
const secEnv = "NOSTR_SECRET_KEY" |
||||
|
||||
var userAgent = fmt.Sprintf("nurl/%s", strings.TrimSpace(version.V)) |
||||
|
||||
func fail(format string, a ...any) { |
||||
_, _ = fmt.Fprintf(os.Stderr, format+"\n", a...) |
||||
os.Exit(1) |
||||
} |
||||
|
||||
func main() { |
||||
if len(os.Args) > 1 && os.Args[1] == "help" { |
||||
fmt.Printf( |
||||
`nurl help: |
||||
|
||||
for nostr http using NIP-98 HTTP authentication: |
||||
|
||||
nurl <url> [file] |
||||
|
||||
if no file is given, the request will be processed as a HTTP GET. |
||||
|
||||
* NIP-98 secret will be expected in the environment variable "%s" |
||||
- if absent, will not be added to the header. |
||||
- endpoint is assumed to not require it if absent. |
||||
- an error will be returned if it was needed. |
||||
|
||||
output will be rendered to stdout |
||||
|
||||
`, secEnv, |
||||
) |
||||
os.Exit(0) |
||||
} |
||||
if len(os.Args) < 2 { |
||||
fail( |
||||
`error: nurl requires minimum 1 arg: <url> |
||||
|
||||
signing nsec (in bech32 format) is expected to be found in %s environment variable. |
||||
|
||||
use "help" to get usage information |
||||
`, secEnv, |
||||
) |
||||
} |
||||
var err error |
||||
var sign signer.I |
||||
if sign, err = GetNIP98Signer(); err != nil { |
||||
log.W.Ln("no signer available:", err) |
||||
} |
||||
var ur *url.URL |
||||
if ur, err = url.Parse(os.Args[1]); chk.E(err) { |
||||
fail("invalid URL: `%s` error: `%s`", os.Args[1], err.Error()) |
||||
} |
||||
log.T.S(ur) |
||||
if len(os.Args) == 2 { |
||||
if err = Get(ur, sign); chk.E(err) { |
||||
fail(err.Error()) |
||||
} |
||||
return |
||||
} |
||||
if err = Post(os.Args[2], ur, sign); chk.E(err) { |
||||
fail(err.Error()) |
||||
} |
||||
} |
||||
|
||||
func GetNIP98Signer() (sign signer.I, err error) { |
||||
nsec := os.Getenv(secEnv) |
||||
var sk []byte |
||||
if len(nsec) == 0 { |
||||
err = fmt.Errorf("no bech32 secret key found in environment variable %s", secEnv) |
||||
return |
||||
} else if sk, err = bech32encoding.NsecToBytes([]byte(nsec)); chk.E(err) { |
||||
err = fmt.Errorf("failed to decode nsec: '%s'", err.Error()) |
||||
return |
||||
} |
||||
var s *p8k.Signer |
||||
if s, err = p8k.New(); chk.E(err) { |
||||
err = fmt.Errorf("failed to create signer: '%s'", err.Error()) |
||||
return |
||||
} |
||||
if err = s.InitSec(sk); chk.E(err) { |
||||
err = fmt.Errorf("failed to init signer: '%s'", err.Error()) |
||||
return |
||||
} |
||||
sign = s |
||||
return |
||||
} |
||||
|
||||
func Get(ur *url.URL, sign signer.I) (err error) { |
||||
log.T.F("GET %s", ur.String()) |
||||
var r *http.Request |
||||
if r, err = http.NewRequest("GET", ur.String(), nil); chk.E(err) { |
||||
return |
||||
} |
||||
r.Header.Add("User-Agent", userAgent) |
||||
if sign != nil { |
||||
if err = httpauth.AddNIP98Header( |
||||
r, ur, "GET", "", sign, 0, |
||||
); chk.E(err) { |
||||
fail(err.Error()) |
||||
} |
||||
} |
||||
client := &http.Client{ |
||||
CheckRedirect: func( |
||||
req *http.Request, |
||||
via []*http.Request, |
||||
) error { |
||||
return http.ErrUseLastResponse |
||||
}, |
||||
} |
||||
var res *http.Response |
||||
if res, err = client.Do(r); chk.E(err) { |
||||
err = fmt.Errorf("request failed: %w", err) |
||||
return |
||||
} |
||||
if _, err = io.Copy(os.Stdout, res.Body); chk.E(err) { |
||||
res.Body.Close() |
||||
return |
||||
} |
||||
res.Body.Close() |
||||
return |
||||
} |
||||
|
||||
func Post(f string, ur *url.URL, sign signer.I) (err error) { |
||||
log.T.F("POST %s", ur.String()) |
||||
var contentLength int64 |
||||
var payload io.ReadCloser |
||||
// get the file path parameters and optional hash
|
||||
var fi os.FileInfo |
||||
if fi, err = os.Stat(f); chk.E(err) { |
||||
return |
||||
} |
||||
var b []byte |
||||
if b, err = os.ReadFile(f); chk.E(err) { |
||||
return |
||||
} |
||||
hb := sha256.Sum256(b) |
||||
h := hex.Enc(hb[:]) |
||||
contentLength = fi.Size() |
||||
if payload, err = os.Open(f); chk.E(err) { |
||||
return |
||||
} |
||||
log.T.F("opened file %s hash %s", f, h) |
||||
var r *http.Request |
||||
r = &http.Request{ |
||||
Method: "POST", |
||||
URL: ur, |
||||
Proto: "HTTP/1.1", |
||||
ProtoMajor: 1, |
||||
ProtoMinor: 1, |
||||
Header: make(http.Header), |
||||
Body: payload, |
||||
ContentLength: contentLength, |
||||
Host: ur.Host, |
||||
} |
||||
r.Header.Add("User-Agent", userAgent) |
||||
if sign != nil { |
||||
if err = httpauth.AddNIP98Header( |
||||
r, ur, "POST", string(h), sign, 0, |
||||
); chk.E(err) { |
||||
fail(err.Error()) |
||||
} |
||||
} |
||||
r.GetBody = func() (rc io.ReadCloser, err error) { |
||||
rc = payload |
||||
return |
||||
} |
||||
client := &http.Client{} |
||||
var res *http.Response |
||||
if res, err = client.Do(r); chk.E(err) { |
||||
return |
||||
} |
||||
defer res.Body.Close() |
||||
if _, err = io.Copy(os.Stdout, res.Body); chk.E(err) { |
||||
return |
||||
} |
||||
return |
||||
} |
||||
@ -0,0 +1,235 @@
@@ -0,0 +1,235 @@
|
||||
// Package main is a simple nostr key miner that uses the fast bitcoin secp256k1
|
||||
// C library to derive npubs with specified prefix/infix/suffix strings present.
|
||||
package main |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/hex" |
||||
"fmt" |
||||
"os" |
||||
"runtime" |
||||
"strings" |
||||
"sync" |
||||
"sync/atomic" |
||||
"time" |
||||
|
||||
"lol.mleku.dev/chk" |
||||
"lol.mleku.dev/log" |
||||
|
||||
"git.mleku.dev/mleku/nostr/crypto/ec/bech32" |
||||
"git.mleku.dev/mleku/nostr/encoders/bech32encoding" |
||||
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k" |
||||
|
||||
"github.com/alexflint/go-arg" |
||||
) |
||||
|
||||
var prefix = append(bech32encoding.PubHRP, '1') |
||||
|
||||
const ( |
||||
PositionBeginning = iota |
||||
PositionContains |
||||
PositionEnding |
||||
) |
||||
|
||||
type Result struct { |
||||
sec []byte |
||||
npub []byte |
||||
pub []byte |
||||
} |
||||
|
||||
var args struct { |
||||
String string `arg:"positional" help:"the string you want to appear in the npub"` |
||||
Position string `arg:"positional" default:"end" help:"[begin|contain|end] default: end"` |
||||
Threads int `help:"number of threads to mine with - defaults to using all CPU threads available"` |
||||
} |
||||
|
||||
func main() { |
||||
arg.MustParse(&args) |
||||
if args.String == "" { |
||||
_, _ = fmt.Fprintln( |
||||
os.Stderr, |
||||
`Usage: vainstr [--threads THREADS] [STRING [POSITION]] |
||||
|
||||
Positional arguments: |
||||
STRING the string you want to appear in the npub |
||||
POSITION [begin|contain|end] default: end |
||||
|
||||
Options: |
||||
--threads THREADS number of threads to mine with - defaults to using all CPU threads available |
||||
--help, -h display this help and exit`, |
||||
) |
||||
os.Exit(0) |
||||
} |
||||
var where int |
||||
canonical := strings.ToLower(args.Position) |
||||
switch { |
||||
case strings.HasPrefix(canonical, "begin"): |
||||
where = PositionBeginning |
||||
case strings.Contains(canonical, "contain"): |
||||
where = PositionContains |
||||
case strings.HasSuffix(canonical, "end"): |
||||
where = PositionEnding |
||||
} |
||||
if args.Threads == 0 { |
||||
args.Threads = runtime.NumCPU() |
||||
} |
||||
if err := Vanity(args.String, where, args.Threads); chk.E(err) { |
||||
log.F.F("error: %s", err) |
||||
} |
||||
} |
||||
|
||||
func Vanity(str string, where int, threads int) (err error) { |
||||
// check the string has valid bech32 ciphers
|
||||
for i := range str { |
||||
wrong := true |
||||
for j := range bech32.Charset { |
||||
if str[i] == bech32.Charset[j] { |
||||
wrong = false |
||||
break |
||||
} |
||||
} |
||||
if wrong { |
||||
return fmt.Errorf( |
||||
"found invalid character '%c' only ones from '%s' allowed\n", |
||||
str[i], bech32.Charset, |
||||
) |
||||
} |
||||
} |
||||
started := time.Now() |
||||
quit := make(chan struct{}) |
||||
resC := make(chan Result) |
||||
|
||||
// Handle interrupt
|
||||
go func() { |
||||
c := make(chan os.Signal, 1) |
||||
<-c |
||||
close(quit) |
||||
log.I.Ln("\rinterrupt signal received") |
||||
os.Exit(0) |
||||
}() |
||||
|
||||
var wg sync.WaitGroup |
||||
var counter int64 |
||||
for i := 0; i < threads; i++ { |
||||
log.D.F("starting up worker %d", i) |
||||
go mine(str, where, quit, resC, &wg, &counter) |
||||
} |
||||
tick := time.NewTicker(time.Second * 5) |
||||
var res Result |
||||
out: |
||||
for { |
||||
select { |
||||
case <-tick.C: |
||||
workingFor := time.Now().Sub(started) |
||||
wm := workingFor % time.Second |
||||
workingFor -= wm |
||||
fmt.Printf( |
||||
" working for %v, attempts %d", |
||||
workingFor, atomic.LoadInt64(&counter), |
||||
) |
||||
case r := <-resC: |
||||
// one of the workers found the solution
|
||||
res = r |
||||
// tell the others to stop
|
||||
close(quit) |
||||
break out |
||||
} |
||||
} |
||||
|
||||
// wait for all workers to stop
|
||||
wg.Wait() |
||||
|
||||
fmt.Printf( |
||||
"\r# generated in %d attempts using %d threads, taking %v ", |
||||
atomic.LoadInt64(&counter), args.Threads, time.Now().Sub(started), |
||||
) |
||||
fmt.Printf( |
||||
"\nHSEC = %s\nHPUB = %s\n", |
||||
hex.EncodeToString(res.sec), |
||||
hex.EncodeToString(res.pub), |
||||
) |
||||
nsec, _ := bech32encoding.BinToNsec(res.sec) |
||||
fmt.Printf("NSEC = %s\nNPUB = %s\n", nsec, res.npub) |
||||
return |
||||
} |
||||
|
||||
func mine( |
||||
str string, where int, quit <-chan struct{}, resC chan Result, wg *sync.WaitGroup, |
||||
counter *int64, |
||||
) { |
||||
wg.Add(1) |
||||
defer wg.Done() |
||||
|
||||
var r Result |
||||
var e error |
||||
found := false |
||||
out: |
||||
for { |
||||
select { |
||||
case <-quit: |
||||
if found { |
||||
// send back the result
|
||||
log.D.Ln("sending back result") |
||||
resC <- r |
||||
log.D.Ln("sent") |
||||
} else { |
||||
log.D.Ln("other thread found it") |
||||
} |
||||
break out |
||||
default: |
||||
} |
||||
atomic.AddInt64(counter, 1) |
||||
r.sec, r.pub, e = Gen() |
||||
if e != nil { |
||||
log.E.Ln("error generating key: '%v' worker stopping", e) |
||||
break out |
||||
} |
||||
if r.npub, e = bech32encoding.BinToNpub(r.pub); e != nil { |
||||
log.E.Ln("fatal error generating npub: %s", e) |
||||
break out |
||||
} |
||||
fmt.Printf("\rgenerating key: %s", r.npub) |
||||
switch where { |
||||
case PositionBeginning: |
||||
if bytes.HasPrefix(r.npub, append(prefix, []byte(str)...)) { |
||||
found = true |
||||
// Signal quit by sending result
|
||||
select { |
||||
case resC <- r: |
||||
default: |
||||
} |
||||
return |
||||
} |
||||
case PositionEnding: |
||||
if bytes.HasSuffix(r.npub, []byte(str)) { |
||||
found = true |
||||
select { |
||||
case resC <- r: |
||||
default: |
||||
} |
||||
return |
||||
} |
||||
case PositionContains: |
||||
if bytes.Contains(r.npub, []byte(str)) { |
||||
found = true |
||||
select { |
||||
case resC <- r: |
||||
default: |
||||
} |
||||
return |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func Gen() (skb, pkb []byte, err error) { |
||||
sign, err := p8k.New() |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
if err = sign.Generate(); chk.E(err) { |
||||
return |
||||
} |
||||
skb, pkb = sign.Sec(), sign.Pub() |
||||
return |
||||
} |
||||
Loading…
Reference in new issue