Add nurl and vainstr CLI tools (v0.39.0)
Some checks failed
Go / build-and-release (push) Has been cancelled
Some checks failed
Go / build-and-release (push) Has been cancelled
- 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>
This commit is contained in:
235
cmd/vainstr/main.go
Normal file
235
cmd/vainstr/main.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user