mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-08 16:48:51 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54c4be10bd | ||
|
|
27f925c05e | ||
|
|
79cb63a1b4 | ||
|
|
5ee0036128 | ||
|
|
316d94166e | ||
|
|
2ca6bb0940 | ||
|
|
ac00c5065f | ||
|
|
441ee9a5ed | ||
|
|
9a41450209 | ||
|
|
dba2ed0b5f | ||
|
|
2079ddf818 | ||
|
|
2135b68106 | ||
|
|
9f98a0aea3 | ||
|
|
1ba39ca7d7 | ||
|
|
262c0c892a | ||
|
|
363bd66a8a | ||
|
|
eccce6dc4a | ||
|
|
31f007ffc2 | ||
|
|
bb45059218 | ||
|
|
71dfe583ed | ||
|
|
84bde7dacd | ||
|
|
81968f6c0c | ||
|
|
f198a46c19 | ||
|
|
c3ea9c15f6 | ||
|
|
8ddb9ce021 | ||
|
|
569d38a137 | ||
|
|
34c189af28 | ||
|
|
ffe2db7f96 | ||
|
|
c5f7926471 | ||
|
|
e008e08105 |
12
README.md
12
README.md
@@ -16,7 +16,7 @@ take a look at the help text that comes in it to learn all possibilities, but he
|
|||||||
|
|
||||||
### make a nostr event with custom content and tags, sign it with a different key and publish it to two relays
|
### make a nostr event with custom content and tags, sign it with a different key and publish it to two relays
|
||||||
```shell
|
```shell
|
||||||
~> nak event --sec 02 -c 'good morning' --tag t=gm wss://nostr-pub.wellorder.net wss://relay.damus.io
|
~> nak event --sec 02 -c 'good morning' --tag t=gm nostr-pub.wellorder.net relay.damus.io
|
||||||
{"id":"e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698632753,"kind":1,"tags":[["t","gm"]],"content":"good morning","sig":"5687c1a97066c349cb3bde0c0719fd1652a13403ba6aca7557b646307ee6718528cd86989db08bf6a7fd04bea0b0b87c1dd1b78c2d21b80b80eebab7f40b8916"}
|
{"id":"e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698632753,"kind":1,"tags":[["t","gm"]],"content":"good morning","sig":"5687c1a97066c349cb3bde0c0719fd1652a13403ba6aca7557b646307ee6718528cd86989db08bf6a7fd04bea0b0b87c1dd1b78c2d21b80b80eebab7f40b8916"}
|
||||||
publishing to wss://nostr-pub.wellorder.net... success.
|
publishing to wss://nostr-pub.wellorder.net... success.
|
||||||
publishing to wss://relay.damus.io... success.
|
publishing to wss://relay.damus.io... success.
|
||||||
@@ -24,7 +24,7 @@ publishing to wss://relay.damus.io... success.
|
|||||||
|
|
||||||
### query a bunch of relays for a tag with a limit of 2 for each, print their content
|
### query a bunch of relays for a tag with a limit of 2 for each, print their content
|
||||||
```shell
|
```shell
|
||||||
~> nak req -k 1 -t t=gm -l 2 wss://nostr.mom wss://nostr.wine wss://nostr-pub.wellorder.net | jq .content
|
~> nak req -k 1 -t t=gm -l 2 nostr.mom nostr.wine nostr-pub.wellorder.net | jq .content
|
||||||
"#GM, you sovereign savage #freeple of the #nostrverse. Let's cause some #nostroversy. "
|
"#GM, you sovereign savage #freeple of the #nostrverse. Let's cause some #nostroversy. "
|
||||||
"ITM slaves!\n#gm https://image.nostr.build/cbbcdf80bfc302a6678ecf9387c87d87deca3e0e288a12e262926c34feb3f6aa.jpg "
|
"ITM slaves!\n#gm https://image.nostr.build/cbbcdf80bfc302a6678ecf9387c87d87deca3e0e288a12e262926c34feb3f6aa.jpg "
|
||||||
"good morning"
|
"good morning"
|
||||||
@@ -34,13 +34,13 @@ publishing to wss://relay.damus.io... success.
|
|||||||
|
|
||||||
### decode a nip19 note1 code, add a relay hint, encode it back to nevent1
|
### decode a nip19 note1 code, add a relay hint, encode it back to nevent1
|
||||||
```shell
|
```shell
|
||||||
~> nak decode note1ttnnrw78wy0hs5fa59yj03yvcu2r4y0xetg9vh7uf4em39n604vsyp37f2 | jq -r .id | nak encode nevent -r wss://nostr.zbd.gg
|
~> nak decode note1ttnnrw78wy0hs5fa59yj03yvcu2r4y0xetg9vh7uf4em39n604vsyp37f2 | jq -r .id | nak encode nevent -r nostr.zbd.gg
|
||||||
nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7
|
nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7
|
||||||
~> nak decode nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7
|
~> nak decode nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7
|
||||||
{
|
{
|
||||||
"id": "5ae731bbc7711f78513da14927c48cc7143a91e6cad0565fdc4d73b8967a7d59",
|
"id": "5ae731bbc7711f78513da14927c48cc7143a91e6cad0565fdc4d73b8967a7d59",
|
||||||
"relays": [
|
"relays": [
|
||||||
"wss://nostr.zbd.gg"
|
"nostr.zbd.gg"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -61,7 +61,7 @@ nak fetch nevent1qqs2e3k48vtrkzjm8vvyzcmsmkf58unrxtq2k4h5yspay6vhcqm4wqcpz9mhxue
|
|||||||
|
|
||||||
### republish an event from one relay to multiple others
|
### republish an event from one relay to multiple others
|
||||||
```shell
|
```shell
|
||||||
~> nak req -i e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a wss://nostr.wine/ wss://nostr-pub.wellorder.net | nak event wss://nostr.wine wss://offchain.pub wss://public.relaying.io wss://eden.nostr.land wss://atlas.nostr.land wss://relayable.org
|
~> nak req -i e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a nostr.wine/ nostr-pub.wellorder.net | nak event nostr.wine offchain.pub public.relaying.io eden.nostr.land atlas.nostr.land relayable.org
|
||||||
{"id":"e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698632753,"kind":1,"tags":[["t","gm"]],"content":"good morning","sig":"5687c1a97066c349cb3bde0c0719fd1652a13403ba6aca7557b646307ee6718528cd86989db08bf6a7fd04bea0b0b87c1dd1b78c2d21b80b80eebab7f40b8916"}
|
{"id":"e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698632753,"kind":1,"tags":[["t","gm"]],"content":"good morning","sig":"5687c1a97066c349cb3bde0c0719fd1652a13403ba6aca7557b646307ee6718528cd86989db08bf6a7fd04bea0b0b87c1dd1b78c2d21b80b80eebab7f40b8916"}
|
||||||
publishing to wss://nostr.wine... failed: msg: blocked: not an active paid member
|
publishing to wss://nostr.wine... failed: msg: blocked: not an active paid member
|
||||||
publishing to wss://offchain.pub... success.
|
publishing to wss://offchain.pub... success.
|
||||||
@@ -79,7 +79,7 @@ invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c
|
|||||||
|
|
||||||
### fetch all quoted events by a given pubkey in their last 100 notes
|
### fetch all quoted events by a given pubkey in their last 100 notes
|
||||||
```shell
|
```shell
|
||||||
nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 wss://relay.damus.io | jq -r '.content | match("nostr:((note1|nevent1)[a-z0-9]+)";"g") | .captures[0].string' | nak decode | jq -cr '{ids: [.id]}' | nak req wss://relay.damus.io
|
nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 relay.damus.io | jq -r '.content | match("nostr:((note1|nevent1)[a-z0-9]+)";"g") | .captures[0].string' | nak decode | jq -cr '{ids: [.id]}' | nak req relay.damus.io
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing to this repository
|
## Contributing to this repository
|
||||||
|
|||||||
188
bunker.go
188
bunker.go
@@ -1,16 +1,20 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
"github.com/nbd-wtf/go-nostr/nip46"
|
"github.com/nbd-wtf/go-nostr/nip46"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v3"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,18 +34,23 @@ var bunker = &cli.Command{
|
|||||||
Name: "prompt-sec",
|
Name: "prompt-sec",
|
||||||
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
|
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: "yes",
|
Name: "authorized-secrets",
|
||||||
Aliases: []string{"y"},
|
Aliases: []string{"s"},
|
||||||
Usage: "always respond to any NIP-46 requests from anyone",
|
Usage: "secrets for which we will always respond",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "authorized-keys",
|
||||||
|
Aliases: []string{"k"},
|
||||||
|
Usage: "pubkeys for which we will always respond",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
// try to connect to the relays here
|
// try to connect to the relays here
|
||||||
qs := url.Values{}
|
qs := url.Values{}
|
||||||
relayURLs := make([]string, 0, c.Args().Len())
|
relayURLs := make([]string, 0, c.Args().Len())
|
||||||
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
|
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
|
||||||
_, relays := connectToAllRelays(c.Context, relayUrls)
|
_, relays := connectToAllRelays(ctx, relayUrls, false)
|
||||||
if len(relays) == 0 {
|
if len(relays) == 0 {
|
||||||
log("failed to connect to any of the given relays.\n")
|
log("failed to connect to any of the given relays.\n")
|
||||||
os.Exit(3)
|
os.Exit(3)
|
||||||
@@ -56,39 +65,128 @@ var bunker = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// gather the secret key
|
// gather the secret key
|
||||||
sec, _, err := gatherSecretKeyOrBunkerFromArguments(c)
|
sec, _, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// other arguments
|
||||||
|
authorizedKeys := c.StringSlice("authorized-keys")
|
||||||
|
authorizedSecrets := c.StringSlice("authorized-secrets")
|
||||||
|
|
||||||
|
// this will be used to auto-authorize the next person who connects who isn't pre-authorized
|
||||||
|
// it will be stored
|
||||||
|
newSecret := randString(12)
|
||||||
|
|
||||||
|
// static information
|
||||||
pubkey, err := nostr.GetPublicKey(sec)
|
pubkey, err := nostr.GetPublicKey(sec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
npub, _ := nip19.EncodePublicKey(pubkey)
|
npub, _ := nip19.EncodePublicKey(pubkey)
|
||||||
bold := color.New(color.Bold).Sprint
|
bold := color.New(color.Bold).Sprint
|
||||||
log("listening at %v:\n pubkey: %s \n npub: %s\n bunker: %s\n\n",
|
italic := color.New(color.Italic).Sprint
|
||||||
|
|
||||||
|
// this function will be called every now and then
|
||||||
|
printBunkerInfo := func() {
|
||||||
|
qs.Set("secret", newSecret)
|
||||||
|
bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode())
|
||||||
|
|
||||||
|
authorizedKeysStr := ""
|
||||||
|
if len(authorizedKeys) != 0 {
|
||||||
|
authorizedKeysStr = "\n authorized keys:\n - " + italic(strings.Join(authorizedKeys, "\n - "))
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizedSecretsStr := ""
|
||||||
|
if len(authorizedSecrets) != 0 {
|
||||||
|
authorizedSecretsStr = "\n authorized secrets:\n - " + italic(strings.Join(authorizedSecrets, "\n - "))
|
||||||
|
}
|
||||||
|
|
||||||
|
preauthorizedFlags := ""
|
||||||
|
for _, k := range authorizedKeys {
|
||||||
|
preauthorizedFlags += " -k " + k
|
||||||
|
}
|
||||||
|
for _, s := range authorizedSecrets {
|
||||||
|
preauthorizedFlags += " -s " + s
|
||||||
|
}
|
||||||
|
|
||||||
|
secretKeyFlag := ""
|
||||||
|
if sec := c.String("sec"); sec != "" {
|
||||||
|
secretKeyFlag = "--sec " + sec
|
||||||
|
}
|
||||||
|
|
||||||
|
relayURLsPossiblyWithoutSchema := make([]string, len(relayURLs))
|
||||||
|
for i, url := range relayURLs {
|
||||||
|
if strings.HasPrefix(url, "wss://") {
|
||||||
|
relayURLsPossiblyWithoutSchema[i] = url[6:]
|
||||||
|
} else {
|
||||||
|
relayURLsPossiblyWithoutSchema[i] = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
restartCommand := fmt.Sprintf("nak bunker %s%s %s",
|
||||||
|
secretKeyFlag,
|
||||||
|
preauthorizedFlags,
|
||||||
|
strings.Join(relayURLsPossiblyWithoutSchema, " "),
|
||||||
|
)
|
||||||
|
|
||||||
|
log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n\n",
|
||||||
bold(relayURLs),
|
bold(relayURLs),
|
||||||
bold(pubkey),
|
bold(pubkey),
|
||||||
bold(npub),
|
bold(npub),
|
||||||
bold(fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode())),
|
authorizedKeysStr,
|
||||||
|
authorizedSecretsStr,
|
||||||
|
color.CyanString(restartCommand),
|
||||||
|
bold(bunkerURI),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
alwaysYes := c.Bool("yes")
|
printBunkerInfo()
|
||||||
|
|
||||||
// subscribe to relays
|
// subscribe to relays
|
||||||
pool := nostr.NewSimplePool(c.Context)
|
pool := nostr.NewSimplePool(ctx)
|
||||||
events := pool.SubMany(c.Context, relayURLs, nostr.Filters{
|
now := nostr.Now()
|
||||||
|
events := pool.SubMany(ctx, relayURLs, nostr.Filters{
|
||||||
{
|
{
|
||||||
Kinds: []int{24133},
|
Kinds: []int{nostr.KindNostrConnect},
|
||||||
Tags: nostr.TagMap{"p": []string{pubkey}},
|
Tags: nostr.TagMap{"p": []string{pubkey}},
|
||||||
|
Since: &now,
|
||||||
|
LimitZero: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
signer := nip46.NewStaticKeySigner(sec)
|
signer := nip46.NewStaticKeySigner(sec)
|
||||||
|
handlerWg := sync.WaitGroup{}
|
||||||
|
printLock := sync.Mutex{}
|
||||||
|
|
||||||
|
// just a gimmick
|
||||||
|
var cancelPreviousBunkerInfoPrint context.CancelFunc
|
||||||
|
_, cancel := context.WithCancel(ctx)
|
||||||
|
cancelPreviousBunkerInfoPrint = cancel
|
||||||
|
|
||||||
|
// asking user for authorization
|
||||||
|
signer.AuthorizeRequest = func(harmless bool, from string, secret string) bool {
|
||||||
|
if secret == newSecret {
|
||||||
|
// store this key
|
||||||
|
authorizedKeys = append(authorizedKeys, from)
|
||||||
|
// discard this and generate a new secret
|
||||||
|
newSecret = randString(12)
|
||||||
|
// print bunker info again after this
|
||||||
|
go func() {
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
printBunkerInfo()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return harmless || slices.Contains(authorizedKeys, from) || slices.Contains(authorizedSecrets, secret)
|
||||||
|
}
|
||||||
|
|
||||||
for ie := range events {
|
for ie := range events {
|
||||||
req, resp, eventResponse, harmless, err := signer.HandleRequest(ie.Event)
|
cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks
|
||||||
|
|
||||||
|
// handle the NIP-46 request event
|
||||||
|
req, resp, eventResponse, err := signer.HandleRequest(ie.Event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log("< failed to handle request from %s: %s", ie.Event.PubKey, err.Error())
|
log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,50 +195,40 @@ var bunker = &cli.Command{
|
|||||||
jresp, _ := json.MarshalIndent(resp, " ", " ")
|
jresp, _ := json.MarshalIndent(resp, " ", " ")
|
||||||
log("~ responding with %s\n", string(jresp))
|
log("~ responding with %s\n", string(jresp))
|
||||||
|
|
||||||
if alwaysYes || harmless || askProceed(ie.Event.PubKey) {
|
handlerWg.Add(len(relayURLs))
|
||||||
for _, relayURL := range relayURLs {
|
for _, relayURL := range relayURLs {
|
||||||
|
go func(relayURL string) {
|
||||||
if relay, _ := pool.EnsureRelay(relayURL); relay != nil {
|
if relay, _ := pool.EnsureRelay(relayURL); relay != nil {
|
||||||
if err := relay.Publish(c.Context, eventResponse); err == nil {
|
err := relay.Publish(ctx, eventResponse)
|
||||||
|
printLock.Lock()
|
||||||
|
if err == nil {
|
||||||
log("* sent response through %s\n", relay.URL)
|
log("* sent response through %s\n", relay.URL)
|
||||||
} else {
|
} else {
|
||||||
log("* failed to send response: %s\n", err)
|
log("* failed to send response: %s\n", err)
|
||||||
}
|
}
|
||||||
|
printLock.Unlock()
|
||||||
|
handlerWg.Done()
|
||||||
}
|
}
|
||||||
|
}(relayURL)
|
||||||
}
|
}
|
||||||
|
handlerWg.Wait()
|
||||||
|
|
||||||
|
// just after handling one request we trigger this
|
||||||
|
go func() {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
cancelPreviousBunkerInfoPrint = cancel
|
||||||
|
// the idea is that we will print the bunker URL again so it is easier to copy-paste by users
|
||||||
|
// but we will only do if the bunker is inactive for more than 5 minutes
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-time.After(time.Minute * 5):
|
||||||
|
fmt.Fprintf(os.Stderr, "\n")
|
||||||
|
printBunkerInfo()
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var allowedSources = make([]string, 0, 2)
|
|
||||||
|
|
||||||
func askProceed(source string) bool {
|
|
||||||
if slices.Contains(allowedSources, source) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "request from %s:\n", color.New(color.Bold, color.FgBlue).Sprint(source))
|
|
||||||
res, err := ask(" proceed to fulfill this request? (yes/no/always from this) (y/n/a): ", "",
|
|
||||||
func(answer string) bool {
|
|
||||||
if answer != "y" && answer != "n" && answer != "a" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch res {
|
|
||||||
case "n":
|
|
||||||
return false
|
|
||||||
case "y":
|
|
||||||
return true
|
|
||||||
case "a":
|
|
||||||
allowedSources = append(allowedSources, source)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
17
count.go
17
count.go
@@ -1,13 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var count = &cli.Command{
|
var count = &cli.Command{
|
||||||
@@ -63,7 +64,7 @@ var count = &cli.Command{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
ArgsUsage: "[relay...]",
|
ArgsUsage: "[relay...]",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
filter := nostr.Filter{}
|
filter := nostr.Filter{}
|
||||||
|
|
||||||
if authors := c.StringSlice("author"); len(authors) > 0 {
|
if authors := c.StringSlice("author"); len(authors) > 0 {
|
||||||
@@ -72,7 +73,11 @@ var count = &cli.Command{
|
|||||||
if ids := c.StringSlice("id"); len(ids) > 0 {
|
if ids := c.StringSlice("id"); len(ids) > 0 {
|
||||||
filter.IDs = ids
|
filter.IDs = ids
|
||||||
}
|
}
|
||||||
if kinds := c.IntSlice("kind"); len(kinds) > 0 {
|
if kinds64 := c.IntSlice("kind"); len(kinds64) > 0 {
|
||||||
|
kinds := make([]int, len(kinds64))
|
||||||
|
for i, v := range kinds64 {
|
||||||
|
kinds[i] = int(v)
|
||||||
|
}
|
||||||
filter.Kinds = kinds
|
filter.Kinds = kinds
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +115,7 @@ var count = &cli.Command{
|
|||||||
filter.Until = &ts
|
filter.Until = &ts
|
||||||
}
|
}
|
||||||
if limit := c.Int("limit"); limit != 0 {
|
if limit := c.Int("limit"); limit != 0 {
|
||||||
filter.Limit = limit
|
filter.Limit = int(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
relays := c.Args().Slice()
|
relays := c.Args().Slice()
|
||||||
@@ -118,12 +123,12 @@ var count = &cli.Command{
|
|||||||
failures := make([]error, 0, len(relays))
|
failures := make([]error, 0, len(relays))
|
||||||
if len(relays) > 0 {
|
if len(relays) > 0 {
|
||||||
for _, relayUrl := range relays {
|
for _, relayUrl := range relays {
|
||||||
relay, err := nostr.RelayConnect(c.Context, relayUrl)
|
relay, err := nostr.RelayConnect(ctx, relayUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
failures = append(failures, err)
|
failures = append(failures, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
count, err := relay.Count(c.Context, nostr.Filters{filter})
|
count, err := relay.Count(ctx, nostr.Filters{filter})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
failures = append(failures, err)
|
failures = append(failures, err)
|
||||||
continue
|
continue
|
||||||
|
|||||||
15
decode.go
15
decode.go
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -8,7 +9,7 @@ import (
|
|||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
sdk "github.com/nbd-wtf/nostr-sdk"
|
sdk "github.com/nbd-wtf/nostr-sdk"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var decode = &cli.Command{
|
var decode = &cli.Command{
|
||||||
@@ -32,8 +33,8 @@ var decode = &cli.Command{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
ArgsUsage: "<npub | nprofile | nip05 | nevent | naddr | nsec>",
|
ArgsUsage: "<npub | nprofile | nip05 | nevent | naddr | nsec>",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for input := range getStdinLinesOrFirstArgument(c.Args().First()) {
|
for input := range getStdinLinesOrArguments(c.Args()) {
|
||||||
if strings.HasPrefix(input, "nostr:") {
|
if strings.HasPrefix(input, "nostr:") {
|
||||||
input = input[6:]
|
input = input[6:]
|
||||||
}
|
}
|
||||||
@@ -49,12 +50,12 @@ var decode = &cli.Command{
|
|||||||
decodeResult.HexResult.PrivateKey = hex.EncodeToString(b)
|
decodeResult.HexResult.PrivateKey = hex.EncodeToString(b)
|
||||||
decodeResult.HexResult.PublicKey = hex.EncodeToString(b)
|
decodeResult.HexResult.PublicKey = hex.EncodeToString(b)
|
||||||
} else {
|
} else {
|
||||||
lineProcessingError(c, "hex string with invalid number of bytes: %d", len(b))
|
ctx = lineProcessingError(ctx, "hex string with invalid number of bytes: %d", len(b))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else if evp := sdk.InputToEventPointer(input); evp != nil {
|
} else if evp := sdk.InputToEventPointer(input); evp != nil {
|
||||||
decodeResult = DecodeResult{EventPointer: evp}
|
decodeResult = DecodeResult{EventPointer: evp}
|
||||||
} else if pp := sdk.InputToProfile(c.Context, input); pp != nil {
|
} else if pp := sdk.InputToProfile(ctx, input); pp != nil {
|
||||||
decodeResult = DecodeResult{ProfilePointer: pp}
|
decodeResult = DecodeResult{ProfilePointer: pp}
|
||||||
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" {
|
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" {
|
||||||
ep := value.(nostr.EntityPointer)
|
ep := value.(nostr.EntityPointer)
|
||||||
@@ -63,7 +64,7 @@ var decode = &cli.Command{
|
|||||||
decodeResult.PrivateKey.PrivateKey = value.(string)
|
decodeResult.PrivateKey.PrivateKey = value.(string)
|
||||||
decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string))
|
decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string))
|
||||||
} else {
|
} else {
|
||||||
lineProcessingError(c, "couldn't decode input '%s': %s", input, err)
|
ctx = lineProcessingError(ctx, "couldn't decode input '%s': %s", input, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ var decode = &cli.Command{
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(c)
|
exitIfLineProcessingError(ctx)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
66
encode.go
66
encode.go
@@ -1,11 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var encode = &cli.Command{
|
var encode = &cli.Command{
|
||||||
@@ -18,20 +19,20 @@ var encode = &cli.Command{
|
|||||||
nak encode nevent <event-id>
|
nak encode nevent <event-id>
|
||||||
nak encode nevent --author <pubkey-hex> --relay <relay-url> --relay <other-relay> <event-id>
|
nak encode nevent --author <pubkey-hex> --relay <relay-url> --relay <other-relay> <event-id>
|
||||||
nak encode nsec <privkey-hex>`,
|
nak encode nsec <privkey-hex>`,
|
||||||
Before: func(c *cli.Context) error {
|
Before: func(ctx context.Context, c *cli.Command) error {
|
||||||
if c.Args().Len() < 1 {
|
if c.Args().Len() < 1 {
|
||||||
return fmt.Errorf("expected more than 1 argument.")
|
return fmt.Errorf("expected more than 1 argument.")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Subcommands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
{
|
{
|
||||||
Name: "npub",
|
Name: "npub",
|
||||||
Usage: "encode a hex public key into bech32 'npub' format",
|
Usage: "encode a hex public key into bech32 'npub' format",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for target := range getStdinLinesOrFirstArgument(c.Args().First()) {
|
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||||
if ok := nostr.IsValidPublicKey(target); !ok {
|
if ok := nostr.IsValidPublicKey(target); !ok {
|
||||||
lineProcessingError(c, "invalid public key: %s", target)
|
ctx = lineProcessingError(ctx, "invalid public key: %s", target)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,17 +43,17 @@ var encode = &cli.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(c)
|
exitIfLineProcessingError(ctx)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "nsec",
|
Name: "nsec",
|
||||||
Usage: "encode a hex private key into bech32 'nsec' format",
|
Usage: "encode a hex private key into bech32 'nsec' format",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for target := range getStdinLinesOrFirstArgument(c.Args().First()) {
|
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||||
if ok := nostr.IsValid32ByteHex(target); !ok {
|
if ok := nostr.IsValid32ByteHex(target); !ok {
|
||||||
lineProcessingError(c, "invalid private key: %s", target)
|
ctx = lineProcessingError(ctx, "invalid private key: %s", target)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ var encode = &cli.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(c)
|
exitIfLineProcessingError(ctx)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -77,15 +78,15 @@ var encode = &cli.Command{
|
|||||||
Usage: "attach relay hints to nprofile code",
|
Usage: "attach relay hints to nprofile code",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for target := range getStdinLinesOrFirstArgument(c.Args().First()) {
|
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||||
if ok := nostr.IsValid32ByteHex(target); !ok {
|
if ok := nostr.IsValid32ByteHex(target); !ok {
|
||||||
lineProcessingError(c, "invalid public key: %s", target)
|
ctx = lineProcessingError(ctx, "invalid public key: %s", target)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
relays := c.StringSlice("relay")
|
relays := c.StringSlice("relay")
|
||||||
if err := validateRelayURLs(relays); err != nil {
|
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ var encode = &cli.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(c)
|
exitIfLineProcessingError(ctx)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -111,13 +112,14 @@ var encode = &cli.Command{
|
|||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "author",
|
Name: "author",
|
||||||
|
Aliases: []string{"a"},
|
||||||
Usage: "attach an author pubkey as a hint to the nevent code",
|
Usage: "attach an author pubkey as a hint to the nevent code",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for target := range getStdinLinesOrFirstArgument(c.Args().First()) {
|
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||||
if ok := nostr.IsValid32ByteHex(target); !ok {
|
if ok := nostr.IsValid32ByteHex(target); !ok {
|
||||||
lineProcessingError(c, "invalid event id: %s", target)
|
ctx = lineProcessingError(ctx, "invalid event id: %s", target)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +131,7 @@ var encode = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
relays := c.StringSlice("relay")
|
relays := c.StringSlice("relay")
|
||||||
if err := validateRelayURLs(relays); err != nil {
|
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +142,7 @@ var encode = &cli.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(c)
|
exitIfLineProcessingError(ctx)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -157,10 +159,10 @@ var encode = &cli.Command{
|
|||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "pubkey",
|
Name: "pubkey",
|
||||||
Usage: "pubkey of the naddr author",
|
Usage: "pubkey of the naddr author",
|
||||||
Aliases: []string{"p"},
|
Aliases: []string{"author", "a", "p"},
|
||||||
Required: true,
|
Required: true,
|
||||||
},
|
},
|
||||||
&cli.Int64Flag{
|
&cli.IntFlag{
|
||||||
Name: "kind",
|
Name: "kind",
|
||||||
Aliases: []string{"k"},
|
Aliases: []string{"k"},
|
||||||
Usage: "kind of referred replaceable event",
|
Usage: "kind of referred replaceable event",
|
||||||
@@ -172,7 +174,7 @@ var encode = &cli.Command{
|
|||||||
Usage: "attach relay hints to naddr code",
|
Usage: "attach relay hints to naddr code",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for d := range getStdinLinesOrBlank() {
|
for d := range getStdinLinesOrBlank() {
|
||||||
pubkey := c.String("pubkey")
|
pubkey := c.String("pubkey")
|
||||||
if ok := nostr.IsValidPublicKey(pubkey); !ok {
|
if ok := nostr.IsValidPublicKey(pubkey); !ok {
|
||||||
@@ -187,34 +189,34 @@ var encode = &cli.Command{
|
|||||||
if d == "" {
|
if d == "" {
|
||||||
d = c.String("identifier")
|
d = c.String("identifier")
|
||||||
if d == "" {
|
if d == "" {
|
||||||
lineProcessingError(c, "\"d\" tag identifier can't be empty")
|
ctx = lineProcessingError(ctx, "\"d\" tag identifier can't be empty")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
relays := c.StringSlice("relay")
|
relays := c.StringSlice("relay")
|
||||||
if err := validateRelayURLs(relays); err != nil {
|
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if npub, err := nip19.EncodeEntity(pubkey, kind, d, relays); err == nil {
|
if npub, err := nip19.EncodeEntity(pubkey, int(kind), d, relays); err == nil {
|
||||||
stdout(npub)
|
stdout(npub)
|
||||||
} else {
|
} else {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(c)
|
exitIfLineProcessingError(ctx)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "note",
|
Name: "note",
|
||||||
Usage: "generate note1 event codes (not recommended)",
|
Usage: "generate note1 event codes (not recommended)",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for target := range getStdinLinesOrFirstArgument(c.Args().First()) {
|
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||||
if ok := nostr.IsValid32ByteHex(target); !ok {
|
if ok := nostr.IsValid32ByteHex(target); !ok {
|
||||||
lineProcessingError(c, "invalid event id: %s", target)
|
ctx = lineProcessingError(ctx, "invalid event id: %s", target)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +227,7 @@ var encode = &cli.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(c)
|
exitIfLineProcessingError(ctx)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
69
event.go
69
event.go
@@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
"github.com/nbd-wtf/go-nostr/nson"
|
"github.com/nbd-wtf/go-nostr/nson"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v3"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ example:
|
|||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "sec",
|
Name: "sec",
|
||||||
Usage: "secret key to sign the event, as hex or nsec",
|
Usage: "secret key to sign the event, as nsec, ncryptsec or hex",
|
||||||
DefaultText: "the key '1'",
|
DefaultText: "the key '1'",
|
||||||
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
||||||
},
|
},
|
||||||
@@ -53,6 +53,31 @@ example:
|
|||||||
Usage: "private key to when communicating with the bunker given on --connect",
|
Usage: "private key to when communicating with the bunker given on --connect",
|
||||||
DefaultText: "a random key",
|
DefaultText: "a random key",
|
||||||
},
|
},
|
||||||
|
// ~ these args are only for the convoluted musig2 signing process
|
||||||
|
// they will be generally copy-shared-pasted across some manual coordination method between participants
|
||||||
|
&cli.UintFlag{
|
||||||
|
Name: "musig",
|
||||||
|
Usage: "number of signers to use for musig2",
|
||||||
|
Value: 1,
|
||||||
|
DefaultText: "1 -- i.e. do not use musig2 at all",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "musig-pubkey",
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "musig-nonce-secret",
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "musig-nonce",
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "musig-partial",
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
|
// ~~~
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "envelope",
|
Name: "envelope",
|
||||||
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
|
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
|
||||||
@@ -116,11 +141,11 @@ example:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
ArgsUsage: "[relay...]",
|
ArgsUsage: "[relay...]",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
// try to connect to the relays here
|
// try to connect to the relays here
|
||||||
var relays []*nostr.Relay
|
var relays []*nostr.Relay
|
||||||
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
|
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
|
||||||
_, relays = connectToAllRelays(c.Context, relayUrls)
|
_, relays = connectToAllRelays(ctx, relayUrls, false)
|
||||||
if len(relays) == 0 {
|
if len(relays) == 0 {
|
||||||
log("failed to connect to any of the given relays.\n")
|
log("failed to connect to any of the given relays.\n")
|
||||||
os.Exit(3)
|
os.Exit(3)
|
||||||
@@ -133,7 +158,7 @@ example:
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(c)
|
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -151,14 +176,14 @@ example:
|
|||||||
|
|
||||||
if stdinEvent != "" {
|
if stdinEvent != "" {
|
||||||
if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
||||||
lineProcessingError(c, "invalid event received from stdin: %s", err)
|
ctx = lineProcessingError(ctx, "invalid event received from stdin: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
kindWasSupplied = strings.Contains(stdinEvent, `"kind"`)
|
kindWasSupplied = strings.Contains(stdinEvent, `"kind"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if kind := c.Int("kind"); slices.Contains(c.FlagNames(), "kind") {
|
if kind := c.Int("kind"); slices.Contains(c.FlagNames(), "kind") {
|
||||||
evt.Kind = kind
|
evt.Kind = int(kind)
|
||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
} else if !kindWasSupplied {
|
} else if !kindWasSupplied {
|
||||||
evt.Kind = 1
|
evt.Kind = 1
|
||||||
@@ -182,9 +207,8 @@ example:
|
|||||||
// tags may also contain extra elements separated with a ";"
|
// tags may also contain extra elements separated with a ";"
|
||||||
tagValues := strings.Split(tagValue, ";")
|
tagValues := strings.Split(tagValue, ";")
|
||||||
tag = append(tag, tagValues...)
|
tag = append(tag, tagValues...)
|
||||||
// ~
|
|
||||||
tags = tags.AppendUnique(tag)
|
|
||||||
}
|
}
|
||||||
|
tags = tags.AppendUnique(tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, etag := range c.StringSlice("e") {
|
for _, etag := range c.StringSlice("e") {
|
||||||
@@ -224,9 +248,24 @@ example:
|
|||||||
|
|
||||||
if evt.Sig == "" || mustRehashAndResign {
|
if evt.Sig == "" || mustRehashAndResign {
|
||||||
if bunker != nil {
|
if bunker != nil {
|
||||||
if err := bunker.SignEvent(c.Context, &evt); err != nil {
|
if err := bunker.SignEvent(ctx, &evt); err != nil {
|
||||||
return fmt.Errorf("failed to sign with bunker: %w", err)
|
return fmt.Errorf("failed to sign with bunker: %w", err)
|
||||||
}
|
}
|
||||||
|
} else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" {
|
||||||
|
pubkeys := c.StringSlice("musig-pubkey")
|
||||||
|
secNonce := c.String("musig-nonce-secret")
|
||||||
|
pubNonces := c.StringSlice("musig-nonce")
|
||||||
|
partialSigs := c.StringSlice("musig-partial")
|
||||||
|
signed, err := performMusig(ctx,
|
||||||
|
sec, &evt, int(numSigners), pubkeys, pubNonces, secNonce, partialSigs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("musig error: %w", err)
|
||||||
|
}
|
||||||
|
if !signed {
|
||||||
|
// we haven't finished signing the event, so the users still have to do more steps
|
||||||
|
// instructions for what to do should have been printed by the performMusig() function
|
||||||
|
return nil
|
||||||
|
}
|
||||||
} else if err := evt.Sign(sec); err != nil {
|
} else if err := evt.Sign(sec); err != nil {
|
||||||
return fmt.Errorf("error signing with provided key: %w", err)
|
return fmt.Errorf("error signing with provided key: %w", err)
|
||||||
}
|
}
|
||||||
@@ -252,7 +291,7 @@ example:
|
|||||||
for _, relay := range relays {
|
for _, relay := range relays {
|
||||||
publish:
|
publish:
|
||||||
log("publishing to %s... ", relay.URL)
|
log("publishing to %s... ", relay.URL)
|
||||||
ctx, cancel := context.WithTimeout(c.Context, 10*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
err := relay.Publish(ctx, evt)
|
err := relay.Publish(ctx, evt)
|
||||||
@@ -268,7 +307,7 @@ example:
|
|||||||
// if the relay is requesting auth and we can auth, let's do it
|
// if the relay is requesting auth and we can auth, let's do it
|
||||||
var pk string
|
var pk string
|
||||||
if bunker != nil {
|
if bunker != nil {
|
||||||
pk, err = bunker.GetPublicKey(c.Context)
|
pk, err = bunker.GetPublicKey(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get public key from bunker: %w", err)
|
return fmt.Errorf("failed to get public key from bunker: %w", err)
|
||||||
}
|
}
|
||||||
@@ -276,9 +315,9 @@ example:
|
|||||||
pk, _ = nostr.GetPublicKey(sec)
|
pk, _ = nostr.GetPublicKey(sec)
|
||||||
}
|
}
|
||||||
log("performing auth as %s... ", pk)
|
log("performing auth as %s... ", pk)
|
||||||
if err := relay.Auth(c.Context, func(evt *nostr.Event) error {
|
if err := relay.Auth(ctx, func(evt *nostr.Event) error {
|
||||||
if bunker != nil {
|
if bunker != nil {
|
||||||
return bunker.SignEvent(c.Context, evt)
|
return bunker.SignEvent(ctx, evt)
|
||||||
}
|
}
|
||||||
return evt.Sign(sec)
|
return evt.Sign(sec)
|
||||||
}); err == nil {
|
}); err == nil {
|
||||||
@@ -298,7 +337,7 @@ example:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(c)
|
exitIfLineProcessingError(ctx)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
129
example_test.go
129
example_test.go
@@ -1,31 +1,140 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ctx = context.Background()
|
||||||
|
|
||||||
func ExampleEventBasic() {
|
func ExampleEventBasic() {
|
||||||
app.Run([]string{"nak", "event", "--ts", "1699485669"})
|
app.Run(ctx, []string{"nak", "event", "--ts", "1699485669"})
|
||||||
// Output:
|
// Output:
|
||||||
// {"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"}
|
// {"kind":1,"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// (for some reason there can only be one test dealing with stdin in the suite otherwise it halts)
|
||||||
|
func ExampleEventParsingFromStdin() {
|
||||||
|
prevStdin := os.Stdin
|
||||||
|
defer func() { os.Stdin = prevStdin }()
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdin = r
|
||||||
|
w.WriteString("{\"content\":\"hello world\"}\n{\"content\":\"hello sun\"}\n")
|
||||||
|
app.Run(ctx, []string{"nak", "event", "-t", "t=spam", "--ts", "1699485669"})
|
||||||
|
// Output:
|
||||||
|
// {"id":"bda134f9077c11973afe6aa5a1cc6f5bcea01c40d318b8f91dcb8e50507cfa52","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello world","sig":"7552454bb8e7944230142634e3e34ac7468bad9b21ed6909da572c611018dff1d14d0792e98b5806f6330edc51e09efa6d0b66a9694dc34606c70f4e580e7493"}
|
||||||
|
// {"id":"879c36ec73acca288825b53585389581d3836e7f0fe4d46e5eba237ca56d6af5","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello sun","sig":"6c7e6b13ebdf931d26acfdd00bec2ec1140ddaf8d1ed61453543a14e729a460fe36c40c488ccb194a0e1ab9511cb6c36741485f501bdb93c39ca4c51bc59cbd4"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleEventComplex() {
|
func ExampleEventComplex() {
|
||||||
app.Run([]string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def?name=foobar;nothing"})
|
app.Run(ctx, []string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def?name=foobar;nothing"})
|
||||||
// Output:
|
// Output:
|
||||||
// {"id":"19aba166dcf354bf5ef64f4afe69ada1eb851495001ee05e07d393ee8c8ea179","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"kind":11,"tags":[["t","spam"],["r","https://abc.def?name=foobar","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"cf452def4a68341c897c3fc96fa34dc6895a5b8cc266d4c041bcdf758ec992ec5adb8b0179e98552aaaf9450526a26d7e62e413b15b1c57e0cfc8db6b29215d7"}
|
// {"id":"19aba166dcf354bf5ef64f4afe69ada1eb851495001ee05e07d393ee8c8ea179","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"kind":11,"tags":[["t","spam"],["r","https://abc.def?name=foobar","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"cf452def4a68341c897c3fc96fa34dc6895a5b8cc266d4c041bcdf758ec992ec5adb8b0179e98552aaaf9450526a26d7e62e413b15b1c57e0cfc8db6b29215d7"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExampleEncode() {
|
||||||
|
app.Run(ctx, []string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
|
||||||
|
app.Run(ctx, []string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
|
||||||
|
app.Run(ctx, []string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822", "a5592173975ded9f836a9572ea8b11a7e16ceb66464d66d50b27163f7f039d2c"})
|
||||||
|
// npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28
|
||||||
|
// nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug
|
||||||
|
// nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug
|
||||||
|
// nprofile1qqs22kfpwwt4mmvlsd4f2uh23vg60ctvadnyvntx659jw93l0upe6tqpz9mhxue69uhk27rpd4cxcefwvdhk64h265a
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleDecode() {
|
||||||
|
app.Run(ctx, []string{"nak", "decode", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"})
|
||||||
|
// Output:
|
||||||
|
// {
|
||||||
|
// "pubkey": "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e",
|
||||||
|
// "kind": 31923,
|
||||||
|
// "identifier": "4cd6cfe7",
|
||||||
|
// "relays": [
|
||||||
|
// "wss://nos.lol"
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// {
|
||||||
|
// "id": "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5",
|
||||||
|
// "relays": [
|
||||||
|
// "wss://pyramid.fiatjaf.com/",
|
||||||
|
// "wss://relay.westernbtc.com/",
|
||||||
|
// "wss://relay.snort.social/",
|
||||||
|
// "wss://atlas.nostr.land/"
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
func ExampleReq() {
|
func ExampleReq() {
|
||||||
app.Run([]string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"})
|
app.Run(ctx, []string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"})
|
||||||
// Output:
|
// Output:
|
||||||
// ["REQ","nak",{"kinds":[1],"authors":["2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"],"limit":18,"#e":["aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"]}]
|
// ["REQ","nak",{"kinds":[1],"authors":["2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"],"limit":18,"#e":["aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"]}]
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleEncodeNpub() {
|
func ExampleReqIdFromRelay() {
|
||||||
app.Run([]string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
|
app.Run(ctx, []string{"nak", "req", "-i", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "wss://nostr.wine"})
|
||||||
// Output:
|
// Output:
|
||||||
// npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28
|
// {"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"kind":1,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleEncodeNprofile() {
|
func ExampleMultipleFetch() {
|
||||||
app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
|
app.Run(ctx, []string{"nak", "fetch", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"})
|
||||||
// Output:
|
// Output:
|
||||||
// nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug
|
// {"id":"9ae5014573fc75ced00b343868d2cd9343ebcbbae50591c6fa8ae1cd99568f05","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1707764605,"kind":31923,"tags":[["d","4cd6cfe7"],["name","Nostr PHX Presents Culture Shock"],["description","Nostr PHX presents Culture Shock the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, food, drinks, conversation, vendors and best of all, a live convert which will stream globally for the world to zap. "],["start","1708185600"],["end","1708228800"],["start_tzid","America/Phoenix"],["p","5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","","host"],["location","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["address","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["g","9tbq1rzn"],["image","https://flockstr.s3.amazonaws.com/event/15vSaiscDhVH1KBXhA0i8"],["about","Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. "],["calendar","31924:5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e:1f238c94"]],"content":"Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. ","sig":"f676629d1414d96b464644de6babde0c96bd21ef9b41ba69ad886a1d13a942b855b715b22ccf38bc07fead18d3bdeee82d9e3825cf6f003fb5ff1766d95c70a0"}
|
||||||
|
// {"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"kind":1,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleKeyPublic() {
|
||||||
|
app.Run(ctx, []string{"nak", "key", "public", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"})
|
||||||
|
// Output:
|
||||||
|
// 70f7120d065870513a6bddb61c8d400ad1e43449b1900ffdb5551e4c421375c8
|
||||||
|
// 718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleKeyDecrypt() {
|
||||||
|
app.Run(ctx, []string{"nak", "key", "decrypt", "ncryptsec1qggfep0m5ythsegkmwfrhhx2zx5gazyhdygvlngcds4wsgdpzfy6nr0exy0pdk0ydwrqyhndt2trtwcgwwag0ja3aqclzptfxxqvprdyaz3qfrmazpecx2ff6dph5mfdjnh5sw8sgecul32eru6xet34", "banana"})
|
||||||
|
// Output:
|
||||||
|
// nsec180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsgyumg0
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleRelay() {
|
||||||
|
app.Run(ctx, []string{"nak", "relay", "relay.nos.social", "pyramid.fiatjaf.com"})
|
||||||
|
// Output:
|
||||||
|
// {
|
||||||
|
// "name": "nos.social strfry relay",
|
||||||
|
// "description": "This is a strfry instance handled by nos.social",
|
||||||
|
// "pubkey": "89ef92b9ebe6dc1e4ea398f6477f227e95429627b0a33dc89b640e137b256be5",
|
||||||
|
// "contact": "https://nos.social",
|
||||||
|
// "supported_nips": [
|
||||||
|
// 1,
|
||||||
|
// 2,
|
||||||
|
// 4,
|
||||||
|
// 9,
|
||||||
|
// 11,
|
||||||
|
// 12,
|
||||||
|
// 16,
|
||||||
|
// 20,
|
||||||
|
// 22,
|
||||||
|
// 28,
|
||||||
|
// 33,
|
||||||
|
// 40
|
||||||
|
// ],
|
||||||
|
// "software": "git+https://github.com/hoytech/strfry.git",
|
||||||
|
// "version": "0.9.4",
|
||||||
|
// "icon": ""
|
||||||
|
// }
|
||||||
|
// {
|
||||||
|
// "name": "the fiatjaf pyramid",
|
||||||
|
// "description": "a relay just for the coolest of the coolest",
|
||||||
|
// "pubkey": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
|
||||||
|
// "contact": "",
|
||||||
|
// "supported_nips": [],
|
||||||
|
// "software": "https://github.com/fiatjaf/khatru",
|
||||||
|
// "version": "n/a",
|
||||||
|
// "limitation": {
|
||||||
|
// "auth_required": false,
|
||||||
|
// "payment_required": false,
|
||||||
|
// "restricted_writes": true
|
||||||
|
// },
|
||||||
|
// "icon": "https://clipart-library.com/images_k/pyramid-transparent/pyramid-transparent-19.png"
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
24
fetch.go
24
fetch.go
@@ -1,15 +1,17 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
sdk "github.com/nbd-wtf/nostr-sdk"
|
sdk "github.com/nbd-wtf/nostr-sdk"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var fetch = &cli.Command{
|
var fetch = &cli.Command{
|
||||||
Name: "fetch",
|
Name: "fetch",
|
||||||
Usage: "fetches events related to the given nip19 code from the included relay hints",
|
Usage: "fetches events related to the given nip19 code from the included relay hints or the author's NIP-65 relays.",
|
||||||
Description: `example usage:
|
Description: `example usage:
|
||||||
nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4
|
nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4
|
||||||
echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`,
|
echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`,
|
||||||
@@ -21,8 +23,8 @@ var fetch = &cli.Command{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
ArgsUsage: "[nip19code]",
|
ArgsUsage: "[nip19code]",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
pool := nostr.NewSimplePool(c.Context)
|
pool := nostr.NewSimplePool(ctx)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
pool.Relays.Range(func(_ string, relay *nostr.Relay) bool {
|
pool.Relays.Range(func(_ string, relay *nostr.Relay) bool {
|
||||||
@@ -31,17 +33,17 @@ var fetch = &cli.Command{
|
|||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for code := range getStdinLinesOrFirstArgument(c.Args().First()) {
|
for code := range getStdinLinesOrArguments(c.Args()) {
|
||||||
filter := nostr.Filter{}
|
filter := nostr.Filter{}
|
||||||
|
|
||||||
prefix, value, err := nip19.Decode(code)
|
prefix, value, err := nip19.Decode(code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lineProcessingError(c, "failed to decode: %s", err)
|
ctx = lineProcessingError(ctx, "failed to decode: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
relays := c.StringSlice("relay")
|
relays := c.StringSlice("relay")
|
||||||
if err := validateRelayURLs(relays); err != nil {
|
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var authorHint string
|
var authorHint string
|
||||||
@@ -75,7 +77,7 @@ var fetch = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if authorHint != "" {
|
if authorHint != "" {
|
||||||
relayList := sdk.FetchRelaysForPubkey(c.Context, pool, authorHint,
|
relayList := sdk.FetchRelaysForPubkey(ctx, pool, authorHint,
|
||||||
"wss://purplepag.es", "wss://relay.damus.io", "wss://relay.noswhere.com",
|
"wss://purplepag.es", "wss://relay.damus.io", "wss://relay.noswhere.com",
|
||||||
"wss://nos.lol", "wss://public.relaying.io", "wss://relay.nostr.band")
|
"wss://nos.lol", "wss://public.relaying.io", "wss://relay.nostr.band")
|
||||||
for _, relayListItem := range relayList {
|
for _, relayListItem := range relayList {
|
||||||
@@ -86,16 +88,16 @@ var fetch = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(relays) == 0 {
|
if len(relays) == 0 {
|
||||||
lineProcessingError(c, "no relay hints found")
|
ctx = lineProcessingError(ctx, "no relay hints found")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) {
|
for ie := range pool.SubManyEose(ctx, relays, nostr.Filters{filter}) {
|
||||||
stdout(ie.Event)
|
stdout(ie.Event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(c)
|
exitIfLineProcessingError(ctx)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
26
go.mod
26
go.mod
@@ -5,37 +5,37 @@ go 1.21
|
|||||||
toolchain go1.21.0
|
toolchain go1.21.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/btcsuite/btcd/btcec/v2 v2.3.3
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
|
||||||
github.com/fatih/color v1.16.0
|
github.com/fatih/color v1.16.0
|
||||||
github.com/mailru/easyjson v0.7.7
|
github.com/mailru/easyjson v0.7.7
|
||||||
github.com/nbd-wtf/go-nostr v0.28.6
|
github.com/nbd-wtf/go-nostr v0.34.2
|
||||||
github.com/nbd-wtf/nostr-sdk v0.0.5
|
github.com/nbd-wtf/nostr-sdk v0.0.5
|
||||||
github.com/urfave/cli/v2 v2.25.7
|
github.com/urfave/cli/v3 v3.0.0-alpha9
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
|
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
|
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||||
github.com/chzyer/logex v1.1.10 // indirect
|
github.com/chzyer/logex v1.1.10 // indirect
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
|
||||||
github.com/fiatjaf/eventstore v0.2.16 // indirect
|
github.com/fiatjaf/eventstore v0.2.16 // indirect
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/gobwas/ws v1.3.1 // indirect
|
github.com/gobwas/ws v1.4.0 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.0.2 // indirect
|
github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/tidwall/gjson v1.17.1 // indirect
|
||||||
github.com/tidwall/gjson v1.17.0 // indirect
|
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
|
||||||
golang.org/x/crypto v0.7.0 // indirect
|
golang.org/x/crypto v0.7.0 // indirect
|
||||||
golang.org/x/sys v0.14.0 // indirect
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
|
golang.org/x/text v0.8.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/urfave/cli/v3 => github.com/fiatjaf/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5
|
||||||
|
|||||||
53
go.sum
53
go.sum
@@ -4,16 +4,16 @@ github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tj
|
|||||||
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
|
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
|
github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
github.com/btcsuite/btcd/btcec/v2 v2.3.3/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ=
|
github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ=
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0=
|
github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0=
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||||
@@ -29,8 +29,6 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O
|
|||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
|
||||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@@ -39,11 +37,13 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn
|
|||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
|
github.com/fiatjaf/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5 h1:yhTRU02Hn1jwq50uUKRxbPZQg0PODe37s73IJNsCJb0=
|
||||||
|
github.com/fiatjaf/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI=
|
||||||
github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg=
|
github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg=
|
||||||
github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA=
|
github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
@@ -52,8 +52,8 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU
|
|||||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
|
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||||
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
@@ -79,8 +79,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/nbd-wtf/go-nostr v0.28.6 h1:iOyzk+6ReG0lvyRAar7w7omFmUk5mnXDyFYkJ+zEjiw=
|
github.com/nbd-wtf/go-nostr v0.34.2 h1:9b4qZ29DhQf9xEWN8/7zfDD868r1jFbpjrR3c+BHc+E=
|
||||||
github.com/nbd-wtf/go-nostr v0.28.6/go.mod h1:aFcp8NO3erHg+glzBfh4wpaMrV1/ahcUPAgITdptxwA=
|
github.com/nbd-wtf/go-nostr v0.34.2/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs=
|
||||||
github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY=
|
github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY=
|
||||||
github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk=
|
github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
@@ -92,32 +92,29 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
|
|||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew=
|
github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||||
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
|
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
||||||
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
|
||||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
|
||||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
@@ -137,11 +134,13 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||||
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -158,3 +157,5 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
113
helpers.go
113
helpers.go
@@ -5,9 +5,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/chzyer/readline"
|
"github.com/chzyer/readline"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
@@ -15,7 +17,7 @@ import (
|
|||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
"github.com/nbd-wtf/go-nostr/nip46"
|
"github.com/nbd-wtf/go-nostr/nip46"
|
||||||
"github.com/nbd-wtf/go-nostr/nip49"
|
"github.com/nbd-wtf/go-nostr/nip49"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -23,7 +25,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var log = func(msg string, args ...any) {
|
var log = func(msg string, args ...any) {
|
||||||
fmt.Fprintf(os.Stderr, msg, args...)
|
fmt.Fprintf(color.Error, msg, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
var stdout = fmt.Println
|
var stdout = fmt.Println
|
||||||
@@ -45,13 +47,21 @@ func getStdinLinesOrBlank() chan string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStdinLinesOrFirstArgument(arg string) chan string {
|
func getStdinLinesOrArguments(args cli.Args) chan string {
|
||||||
|
return getStdinLinesOrArgumentsFromSlice(args.Slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStdinLinesOrArgumentsFromSlice(args []string) chan string {
|
||||||
// try the first argument
|
// try the first argument
|
||||||
if arg != "" {
|
if len(args) > 0 {
|
||||||
single := make(chan string, 1)
|
argsCh := make(chan string, 1)
|
||||||
single <- arg
|
go func() {
|
||||||
close(single)
|
for _, arg := range args {
|
||||||
return single
|
argsCh <- arg
|
||||||
|
}
|
||||||
|
close(argsCh)
|
||||||
|
}()
|
||||||
|
return argsCh
|
||||||
}
|
}
|
||||||
|
|
||||||
// try the stdin
|
// try the stdin
|
||||||
@@ -83,8 +93,11 @@ func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateRelayURLs(wsurls []string) error {
|
func normalizeAndValidateRelayURLs(wsurls []string) error {
|
||||||
for _, wsurl := range wsurls {
|
for i, wsurl := range wsurls {
|
||||||
|
wsurl = nostr.NormalizeURL(wsurl)
|
||||||
|
wsurls[i] = wsurl
|
||||||
|
|
||||||
u, err := url.Parse(wsurl)
|
u, err := url.Parse(wsurl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid relay url '%s': %s", wsurl, err)
|
return fmt.Errorf("invalid relay url '%s': %s", wsurl, err)
|
||||||
@@ -105,13 +118,48 @@ func validateRelayURLs(wsurls []string) error {
|
|||||||
func connectToAllRelays(
|
func connectToAllRelays(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
relayUrls []string,
|
relayUrls []string,
|
||||||
|
forcePreAuth bool,
|
||||||
opts ...nostr.PoolOption,
|
opts ...nostr.PoolOption,
|
||||||
) (*nostr.SimplePool, []*nostr.Relay) {
|
) (*nostr.SimplePool, []*nostr.Relay) {
|
||||||
relays := make([]*nostr.Relay, 0, len(relayUrls))
|
relays := make([]*nostr.Relay, 0, len(relayUrls))
|
||||||
pool := nostr.NewSimplePool(ctx, opts...)
|
pool := nostr.NewSimplePool(ctx, opts...)
|
||||||
|
relayLoop:
|
||||||
for _, url := range relayUrls {
|
for _, url := range relayUrls {
|
||||||
log("connecting to %s... ", url)
|
log("connecting to %s... ", url)
|
||||||
if relay, err := pool.EnsureRelay(url); err == nil {
|
if relay, err := pool.EnsureRelay(url); err == nil {
|
||||||
|
if forcePreAuth {
|
||||||
|
log("waiting for auth challenge... ")
|
||||||
|
signer := opts[0].(nostr.WithAuthHandler)
|
||||||
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
challengeWaitLoop:
|
||||||
|
for {
|
||||||
|
// beginhack
|
||||||
|
// here starts the biggest and ugliest hack of this codebase
|
||||||
|
if err := relay.Auth(ctx, func(authEvent *nostr.Event) error {
|
||||||
|
challengeTag := authEvent.Tags.GetFirst([]string{"challenge", ""})
|
||||||
|
if (*challengeTag)[1] == "" {
|
||||||
|
return fmt.Errorf("auth not received yet *****")
|
||||||
|
}
|
||||||
|
return signer(authEvent)
|
||||||
|
}); err == nil {
|
||||||
|
// auth succeeded
|
||||||
|
break challengeWaitLoop
|
||||||
|
} else {
|
||||||
|
// auth failed
|
||||||
|
if strings.HasSuffix(err.Error(), "auth not received yet *****") {
|
||||||
|
// it failed because we didn't receive the challenge yet, so keep waiting
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue challengeWaitLoop
|
||||||
|
} else {
|
||||||
|
// it failed for some other reason, so skip this relay
|
||||||
|
log(err.Error() + "\n")
|
||||||
|
continue relayLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endhack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
relays = append(relays, relay)
|
relays = append(relays, relay)
|
||||||
log("ok.\n")
|
log("ok.\n")
|
||||||
} else {
|
} else {
|
||||||
@@ -121,18 +169,18 @@ func connectToAllRelays(
|
|||||||
return pool, relays
|
return pool, relays
|
||||||
}
|
}
|
||||||
|
|
||||||
func lineProcessingError(c *cli.Context, msg string, args ...any) {
|
func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context {
|
||||||
c.Context = context.WithValue(c.Context, LINE_PROCESSING_ERROR, true)
|
|
||||||
log(msg+"\n", args...)
|
log(msg+"\n", args...)
|
||||||
|
return context.WithValue(ctx, LINE_PROCESSING_ERROR, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func exitIfLineProcessingError(c *cli.Context) {
|
func exitIfLineProcessingError(ctx context.Context) {
|
||||||
if val := c.Context.Value(LINE_PROCESSING_ERROR); val != nil && val.(bool) {
|
if val := ctx.Value(LINE_PROCESSING_ERROR); val != nil && val.(bool) {
|
||||||
os.Exit(123)
|
os.Exit(123)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.BunkerClient, error) {
|
func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *nip46.BunkerClient, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if bunkerURL := c.String("connect"); bunkerURL != "" {
|
if bunkerURL := c.String("connect"); bunkerURL != "" {
|
||||||
@@ -142,7 +190,9 @@ func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.Bunker
|
|||||||
} else {
|
} else {
|
||||||
clientKey = nostr.GeneratePrivateKey()
|
clientKey = nostr.GeneratePrivateKey()
|
||||||
}
|
}
|
||||||
bunker, err := nip46.ConnectBunker(c.Context, clientKey, bunkerURL, nil)
|
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {
|
||||||
|
fmt.Fprintf(color.Error, color.CyanString("[nip46]: open the following URL: %s"), s)
|
||||||
|
})
|
||||||
return "", bunker, err
|
return "", bunker, err
|
||||||
}
|
}
|
||||||
sec := c.String("sec")
|
sec := c.String("sec")
|
||||||
@@ -160,7 +210,7 @@ func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.Bunker
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("failed to decrypt: %w", err)
|
return "", nil, fmt.Errorf("failed to decrypt: %w", err)
|
||||||
}
|
}
|
||||||
} else if bsec, err := hex.DecodeString(strings.Repeat("0", 64-len(sec)) + sec); err == nil {
|
} else if bsec, err := hex.DecodeString(leftPadKey(sec)); err == nil {
|
||||||
sec = hex.EncodeToString(bsec)
|
sec = hex.EncodeToString(bsec)
|
||||||
} else if prefix, hexvalue, err := nip19.Decode(sec); err != nil {
|
} else if prefix, hexvalue, err := nip19.Decode(sec); err != nil {
|
||||||
return "", nil, fmt.Errorf("invalid nsec: %w", err)
|
return "", nil, fmt.Errorf("invalid nsec: %w", err)
|
||||||
@@ -174,7 +224,7 @@ func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.Bunker
|
|||||||
return sec, nil, nil
|
return sec, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func promptDecrypt(ncryptsec1 string) (string, error) {
|
func promptDecrypt(ncryptsec string) (string, error) {
|
||||||
for i := 1; i < 4; i++ {
|
for i := 1; i < 4; i++ {
|
||||||
var attemptStr string
|
var attemptStr string
|
||||||
if i > 1 {
|
if i > 1 {
|
||||||
@@ -184,7 +234,7 @@ func promptDecrypt(ncryptsec1 string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
sec, err := nip49.Decrypt(ncryptsec1, password)
|
sec, err := nip49.Decrypt(ncryptsec, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -193,18 +243,9 @@ func promptDecrypt(ncryptsec1 string) (string, error) {
|
|||||||
return "", fmt.Errorf("couldn't decrypt private key")
|
return "", fmt.Errorf("couldn't decrypt private key")
|
||||||
}
|
}
|
||||||
|
|
||||||
func ask(msg string, defaultValue string, shouldAskAgain func(answer string) bool) (string, error) {
|
|
||||||
return _ask(&readline.Config{
|
|
||||||
Stdout: os.Stderr,
|
|
||||||
Prompt: color.YellowString(msg),
|
|
||||||
InterruptPrompt: "^C",
|
|
||||||
DisableAutoSaveHistory: true,
|
|
||||||
}, msg, defaultValue, shouldAskAgain)
|
|
||||||
}
|
|
||||||
|
|
||||||
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {
|
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {
|
||||||
config := &readline.Config{
|
config := &readline.Config{
|
||||||
Stdout: os.Stderr,
|
Stdout: color.Error,
|
||||||
Prompt: color.YellowString(msg),
|
Prompt: color.YellowString(msg),
|
||||||
InterruptPrompt: "^C",
|
InterruptPrompt: "^C",
|
||||||
DisableAutoSaveHistory: true,
|
DisableAutoSaveHistory: true,
|
||||||
@@ -233,3 +274,17 @@ func _ask(config *readline.Config, msg string, defaultValue string, shouldAskAga
|
|||||||
return answer, err
|
return answer, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
|
||||||
|
func randString(n int) string {
|
||||||
|
b := make([]byte, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func leftPadKey(k string) string {
|
||||||
|
return strings.Repeat("0", 64-len(k)) + k
|
||||||
|
}
|
||||||
|
|||||||
183
key.go
183
key.go
@@ -1,24 +1,32 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
"github.com/nbd-wtf/go-nostr/nip49"
|
"github.com/nbd-wtf/go-nostr/nip49"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var key = &cli.Command{
|
var key = &cli.Command{
|
||||||
Name: "key",
|
Name: "key",
|
||||||
Usage: "operations on secret keys: generate, derive, encrypt, decrypt.",
|
Usage: "operations on secret keys: generate, derive, encrypt, decrypt.",
|
||||||
Description: ``,
|
Description: ``,
|
||||||
Subcommands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
generate,
|
generate,
|
||||||
public,
|
public,
|
||||||
encrypt,
|
encrypt,
|
||||||
decrypt,
|
decrypt,
|
||||||
|
combine,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +34,7 @@ var generate = &cli.Command{
|
|||||||
Name: "generate",
|
Name: "generate",
|
||||||
Usage: "generates a secret key",
|
Usage: "generates a secret key",
|
||||||
Description: ``,
|
Description: ``,
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
sec := nostr.GeneratePrivateKey()
|
sec := nostr.GeneratePrivateKey()
|
||||||
stdout(sec)
|
stdout(sec)
|
||||||
return nil
|
return nil
|
||||||
@@ -38,11 +46,11 @@ var public = &cli.Command{
|
|||||||
Usage: "computes a public key from a secret key",
|
Usage: "computes a public key from a secret key",
|
||||||
Description: ``,
|
Description: ``,
|
||||||
ArgsUsage: "[secret]",
|
ArgsUsage: "[secret]",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c, c.Args().First()) {
|
for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) {
|
||||||
pubkey, err := nostr.GetPublicKey(sec)
|
pubkey, err := nostr.GetPublicKey(sec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lineProcessingError(c, "failed to derive public key: %s", err)
|
ctx = lineProcessingError(ctx, "failed to derive public key: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
stdout(pubkey)
|
stdout(pubkey)
|
||||||
@@ -64,24 +72,23 @@ var encrypt = &cli.Command{
|
|||||||
DefaultText: "16",
|
DefaultText: "16",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
var content string
|
keys := make([]string, 0, 1)
|
||||||
var password string
|
var password string
|
||||||
switch c.Args().Len() {
|
switch c.Args().Len() {
|
||||||
case 1:
|
case 1:
|
||||||
content = ""
|
|
||||||
password = c.Args().Get(0)
|
password = c.Args().Get(0)
|
||||||
case 2:
|
case 2:
|
||||||
content = c.Args().Get(0)
|
keys = append(keys, c.Args().Get(0))
|
||||||
password = c.Args().Get(1)
|
password = c.Args().Get(1)
|
||||||
}
|
}
|
||||||
if password == "" {
|
if password == "" {
|
||||||
return fmt.Errorf("no password given")
|
return fmt.Errorf("no password given")
|
||||||
}
|
}
|
||||||
for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c, content) {
|
for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, keys) {
|
||||||
ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02)
|
ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lineProcessingError(c, "failed to encrypt: %s", err)
|
ctx = lineProcessingError(ctx, "failed to encrypt: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
stdout(ncryptsec)
|
stdout(ncryptsec)
|
||||||
@@ -95,50 +102,176 @@ var decrypt = &cli.Command{
|
|||||||
Usage: "takes an ncrypsec and a password and decrypts it into an nsec",
|
Usage: "takes an ncrypsec and a password and decrypts it into an nsec",
|
||||||
Description: `uses the NIP-49 standard.`,
|
Description: `uses the NIP-49 standard.`,
|
||||||
ArgsUsage: "<ncryptsec-code> <password>",
|
ArgsUsage: "<ncryptsec-code> <password>",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
var content string
|
var ncryptsec string
|
||||||
var password string
|
var password string
|
||||||
switch c.Args().Len() {
|
switch c.Args().Len() {
|
||||||
case 1:
|
|
||||||
content = ""
|
|
||||||
password = c.Args().Get(0)
|
|
||||||
case 2:
|
case 2:
|
||||||
content = c.Args().Get(0)
|
ncryptsec = c.Args().Get(0)
|
||||||
password = c.Args().Get(1)
|
password = c.Args().Get(1)
|
||||||
}
|
|
||||||
if password == "" {
|
if password == "" {
|
||||||
return fmt.Errorf("no password given")
|
return fmt.Errorf("no password given")
|
||||||
}
|
}
|
||||||
for ncryptsec := range getStdinLinesOrFirstArgument(content) {
|
|
||||||
sec, err := nip49.Decrypt(ncryptsec, password)
|
sec, err := nip49.Decrypt(ncryptsec, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lineProcessingError(c, "failed to decrypt: %s", err)
|
return fmt.Errorf("failed to decrypt: %s", err)
|
||||||
|
}
|
||||||
|
nsec, _ := nip19.EncodePrivateKey(sec)
|
||||||
|
stdout(nsec)
|
||||||
|
return nil
|
||||||
|
case 1:
|
||||||
|
if arg := c.Args().Get(0); strings.HasPrefix(arg, "ncryptsec1") {
|
||||||
|
ncryptsec = arg
|
||||||
|
if res, err := promptDecrypt(ncryptsec); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
stdout(res)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
password = c.Args().Get(0)
|
||||||
|
for ncryptsec := range getStdinLinesOrArgumentsFromSlice([]string{ncryptsec}) {
|
||||||
|
sec, err := nip49.Decrypt(ncryptsec, password)
|
||||||
|
if err != nil {
|
||||||
|
ctx = lineProcessingError(ctx, "failed to decrypt: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
nsec, _ := nip19.EncodePrivateKey(sec)
|
nsec, _ := nip19.EncodePrivateKey(sec)
|
||||||
stdout(nsec)
|
stdout(nsec)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid number of arguments")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSecretKeyFromStdinLinesOrFirstArgument(c *cli.Context, content string) chan string {
|
var combine = &cli.Command{
|
||||||
|
Name: "combine",
|
||||||
|
Usage: "combines two or more pubkeys using musig2",
|
||||||
|
Description: `The public keys must have 33 bytes (66 characters hex), with the 02 or 03 prefix. It is common in Nostr to drop that first byte, so you'll have to derive the public keys again from the private keys in order to get it back.
|
||||||
|
|
||||||
|
However, if the intent is to check if two existing Nostr pubkeys match a given combined pubkey, then it might be sufficient to calculate the combined key for all the possible combinations of pubkeys in the input.`,
|
||||||
|
ArgsUsage: "[pubkey...]",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
type Combination struct {
|
||||||
|
Variants []string `json:"input_variants"`
|
||||||
|
Output struct {
|
||||||
|
XOnly string `json:"x_only"`
|
||||||
|
Variant string `json:"variant"`
|
||||||
|
} `json:"combined_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
Keys []string `json:"keys"`
|
||||||
|
Combinations []Combination `json:"combinations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
result := Result{}
|
||||||
|
|
||||||
|
result.Keys = c.Args().Slice()
|
||||||
|
keyGroups := make([][]*btcec.PublicKey, 0, len(result.Keys))
|
||||||
|
|
||||||
|
for i, keyhex := range result.Keys {
|
||||||
|
keyb, err := hex.DecodeString(keyhex)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing key %s: %w", keyhex, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(keyb) == 32 /* we'll use both the 02 and the 03 prefix versions */ {
|
||||||
|
group := make([]*btcec.PublicKey, 2)
|
||||||
|
for i, prefix := range []byte{0x02, 0x03} {
|
||||||
|
pubk, err := btcec.ParsePubKey(append([]byte{prefix}, keyb...))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error parsing key %s: %s", keyhex, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group[i] = pubk
|
||||||
|
}
|
||||||
|
keyGroups = append(keyGroups, group)
|
||||||
|
} else /* assume it's 33 */ {
|
||||||
|
pubk, err := btcec.ParsePubKey(keyb)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing key %s: %w", keyhex, err)
|
||||||
|
}
|
||||||
|
keyGroups = append(keyGroups, []*btcec.PublicKey{pubk})
|
||||||
|
|
||||||
|
// remove the leading byte from the output just so it is all uniform
|
||||||
|
result.Keys[i] = result.Keys[i][2:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Combinations = make([]Combination, 0, 16)
|
||||||
|
|
||||||
|
var fn func(prepend int, curr []int)
|
||||||
|
fn = func(prepend int, curr []int) {
|
||||||
|
curr = append([]int{prepend}, curr...)
|
||||||
|
if len(curr) == len(keyGroups) {
|
||||||
|
combi := Combination{
|
||||||
|
Variants: make([]string, len(keyGroups)),
|
||||||
|
}
|
||||||
|
|
||||||
|
combining := make([]*btcec.PublicKey, len(keyGroups))
|
||||||
|
for g, altKeys := range keyGroups {
|
||||||
|
altKey := altKeys[curr[g]]
|
||||||
|
variant := secp256k1.PubKeyFormatCompressedEven
|
||||||
|
if altKey.Y().Bit(0) == 1 {
|
||||||
|
variant = secp256k1.PubKeyFormatCompressedOdd
|
||||||
|
}
|
||||||
|
combi.Variants[g] = hex.EncodeToString([]byte{variant})
|
||||||
|
combining[g] = altKey
|
||||||
|
}
|
||||||
|
|
||||||
|
agg, _, _, err := musig2.AggregateKeys(combining, true)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error aggregating: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serialized := agg.FinalKey.SerializeCompressed()
|
||||||
|
combi.Output.XOnly = hex.EncodeToString(serialized[1:])
|
||||||
|
combi.Output.Variant = hex.EncodeToString(serialized[0:1])
|
||||||
|
result.Combinations = append(result.Combinations, combi)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fn(0, curr)
|
||||||
|
if len(keyGroups[len(keyGroups)-len(curr)-1]) > 1 {
|
||||||
|
fn(1, curr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn(0, nil)
|
||||||
|
if len(keyGroups[len(keyGroups)-1]) > 1 {
|
||||||
|
fn(1, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, _ := json.MarshalIndent(result, "", " ")
|
||||||
|
fmt.Println(string(res))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, c *cli.Command, keys []string) chan string {
|
||||||
ch := make(chan string)
|
ch := make(chan string)
|
||||||
go func() {
|
go func() {
|
||||||
for sec := range getStdinLinesOrFirstArgument(content) {
|
for sec := range getStdinLinesOrArgumentsFromSlice(keys) {
|
||||||
if sec == "" {
|
if sec == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(sec, "nsec1") {
|
if strings.HasPrefix(sec, "nsec1") {
|
||||||
_, data, err := nip19.Decode(sec)
|
_, data, err := nip19.Decode(sec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lineProcessingError(c, "invalid nsec code: %s", err)
|
ctx = lineProcessingError(ctx, "invalid nsec code: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sec = data.(string)
|
sec = data.(string)
|
||||||
}
|
}
|
||||||
|
sec = leftPadKey(sec)
|
||||||
if !nostr.IsValid32ByteHex(sec) {
|
if !nostr.IsValid32ByteHex(sec) {
|
||||||
lineProcessingError(c, "invalid hex secret key")
|
ctx = lineProcessingError(ctx, "invalid hex key")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ch <- sec
|
ch <- sec
|
||||||
|
|||||||
14
main.go
14
main.go
@@ -1,17 +1,17 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var q int
|
var app = &cli.Command{
|
||||||
|
|
||||||
var app = &cli.App{
|
|
||||||
Name: "nak",
|
Name: "nak",
|
||||||
Suggest: true,
|
Suggest: true,
|
||||||
UseShortOptionHandling: true,
|
UseShortOptionHandling: true,
|
||||||
|
AllowFlagsAfterArguments: true,
|
||||||
Usage: "the nostr army knife command-line tool",
|
Usage: "the nostr army knife command-line tool",
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
req,
|
req,
|
||||||
@@ -29,9 +29,9 @@ var app = &cli.App{
|
|||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "quiet",
|
Name: "quiet",
|
||||||
Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout",
|
Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout",
|
||||||
Count: &q,
|
|
||||||
Aliases: []string{"q"},
|
Aliases: []string{"q"},
|
||||||
Action: func(ctx *cli.Context, b bool) error {
|
Action: func(ctx context.Context, c *cli.Command, b bool) error {
|
||||||
|
q := c.Count("quiet")
|
||||||
if q >= 1 {
|
if q >= 1 {
|
||||||
log = func(msg string, args ...any) {}
|
log = func(msg string, args ...any) {}
|
||||||
if q >= 2 {
|
if q >= 2 {
|
||||||
@@ -45,7 +45,7 @@ var app = &cli.App{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := app.Run(os.Args); err != nil {
|
if err := app.Run(context.Background(), os.Args); err != nil {
|
||||||
stdout(err)
|
stdout(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
331
musig2.go
Normal file
331
musig2.go
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func performMusig(
|
||||||
|
ctx context.Context,
|
||||||
|
sec string,
|
||||||
|
evt *nostr.Event,
|
||||||
|
numSigners int,
|
||||||
|
keys []string,
|
||||||
|
nonces []string,
|
||||||
|
secNonce string,
|
||||||
|
partialSigs []string,
|
||||||
|
) (signed bool, err error) {
|
||||||
|
// preprocess data received
|
||||||
|
secb, err := hex.DecodeString(sec)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
seck, pubk := btcec.PrivKeyFromBytes(secb)
|
||||||
|
|
||||||
|
knownSigners := make([]*btcec.PublicKey, 0, numSigners)
|
||||||
|
includesUs := false
|
||||||
|
for _, hexpub := range keys {
|
||||||
|
bpub, err := hex.DecodeString(hexpub)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
spub, err := btcec.ParsePubKey(bpub)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
knownSigners = append(knownSigners, spub)
|
||||||
|
|
||||||
|
if spub.IsEqual(pubk) {
|
||||||
|
includesUs = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !includesUs {
|
||||||
|
knownSigners = append(knownSigners, pubk)
|
||||||
|
}
|
||||||
|
|
||||||
|
knownNonces := make([][66]byte, 0, numSigners)
|
||||||
|
for _, hexnonce := range nonces {
|
||||||
|
bnonce, err := hex.DecodeString(hexnonce)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if len(bnonce) != 66 {
|
||||||
|
return false, fmt.Errorf("nonce is not 66 bytes: %s", hexnonce)
|
||||||
|
}
|
||||||
|
var b66nonce [66]byte
|
||||||
|
copy(b66nonce[:], bnonce)
|
||||||
|
knownNonces = append(knownNonces, b66nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
knownPartialSigs := make([]*musig2.PartialSignature, 0, numSigners)
|
||||||
|
for _, hexps := range partialSigs {
|
||||||
|
bps, err := hex.DecodeString(hexps)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
var ps musig2.PartialSignature
|
||||||
|
if err := ps.Decode(bytes.NewBuffer(bps)); err != nil {
|
||||||
|
return false, fmt.Errorf("invalid partial signature %s: %w", hexps, err)
|
||||||
|
}
|
||||||
|
knownPartialSigs = append(knownPartialSigs, &ps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the context
|
||||||
|
var mctx *musig2.Context
|
||||||
|
if len(knownSigners) < numSigners {
|
||||||
|
// we don't know all the signers yet
|
||||||
|
mctx, err = musig2.NewContext(seck, true,
|
||||||
|
musig2.WithNumSigners(numSigners),
|
||||||
|
musig2.WithEarlyNonceGen(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to create signing context with %d unknown signers: %w",
|
||||||
|
numSigners, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// we know all the signers
|
||||||
|
mctx, err = musig2.NewContext(seck, true,
|
||||||
|
musig2.WithKnownSigners(knownSigners),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to create signing context with %d known signers: %w",
|
||||||
|
len(knownSigners), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nonce generation phase -- for sharing
|
||||||
|
if len(knownSigners) < numSigners {
|
||||||
|
// if we don't have all the signers we just generate a nonce and yield it to the next people
|
||||||
|
nonce, err := mctx.EarlySessionNonce()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "the following code should be saved secretly until the next step an included with --musig-nonce-secret:\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n\n", base64.StdEncoding.EncodeToString(nonce.SecNonce[:]))
|
||||||
|
|
||||||
|
knownNonces = append(knownNonces, nonce.PubNonce)
|
||||||
|
printPublicCommandForNextPeer(evt, numSigners, knownSigners, knownNonces, nil, false)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we got here we have all the pubkeys, so we can print the combined key
|
||||||
|
if comb, err := mctx.CombinedKey(); err != nil {
|
||||||
|
return false, fmt.Errorf("failed to combine keys (after %d signers): %w", len(knownSigners), err)
|
||||||
|
} else {
|
||||||
|
evt.PubKey = hex.EncodeToString(comb.SerializeCompressed()[1:])
|
||||||
|
evt.ID = evt.GetID()
|
||||||
|
fmt.Fprintf(os.Stderr, "combined key: %x\n\n", comb.SerializeCompressed())
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have all the signers, which means we must also have all the nonces
|
||||||
|
var session *musig2.Session
|
||||||
|
if len(keys) == numSigners-1 {
|
||||||
|
// if we were the last to include our key, that means we have to include our nonce here to
|
||||||
|
// i.e. we didn't input our own pub nonce in the parameters
|
||||||
|
session, err = mctx.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to create session as the last peer to include our key: %w", err)
|
||||||
|
}
|
||||||
|
knownNonces = append(knownNonces, session.PublicNonce())
|
||||||
|
} else {
|
||||||
|
// otherwise we have included our own nonce in the parameters (from copypasting) but must
|
||||||
|
// also include the secret nonce that wasn't shared with peers
|
||||||
|
if secNonce == "" {
|
||||||
|
return false, fmt.Errorf("missing --musig-nonce-secret value")
|
||||||
|
}
|
||||||
|
secNonceB, err := base64.StdEncoding.DecodeString(secNonce)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("invalid --musig-nonce-secret: %w", err)
|
||||||
|
}
|
||||||
|
var secNonce97 [97]byte
|
||||||
|
copy(secNonce97[:], secNonceB)
|
||||||
|
session, err = mctx.NewSession(musig2.WithPreGeneratedNonce(&musig2.Nonces{
|
||||||
|
SecNonce: secNonce97,
|
||||||
|
PubNonce: secNonceToPubNonce(secNonce97),
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to create signing session with secret nonce: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var noncesOk bool
|
||||||
|
for _, b66nonce := range knownNonces {
|
||||||
|
if b66nonce == session.PublicNonce() {
|
||||||
|
// don't add our own nonce
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
noncesOk, err = session.RegisterPubNonce(b66nonce)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to register nonce: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !noncesOk {
|
||||||
|
return false, fmt.Errorf("we've registered all the nonces we had but at least one is missing, this shouldn't happen")
|
||||||
|
}
|
||||||
|
|
||||||
|
// signing phase
|
||||||
|
// we always have to sign, so let's do this
|
||||||
|
id := evt.GetID()
|
||||||
|
hash, _ := hex.DecodeString(id)
|
||||||
|
var msg32 [32]byte
|
||||||
|
copy(msg32[:], hash)
|
||||||
|
partialSig, err := session.Sign(msg32) // this will already include our sig in the bundle
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to produce partial signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(knownPartialSigs)+1 < len(knownSigners) {
|
||||||
|
// still missing some signatures
|
||||||
|
knownPartialSigs = append(knownPartialSigs, partialSig) // we include ours here just so it's printed
|
||||||
|
printPublicCommandForNextPeer(evt, numSigners, knownSigners, knownNonces, knownPartialSigs, true)
|
||||||
|
return false, nil
|
||||||
|
} else {
|
||||||
|
// we have all signatures
|
||||||
|
for _, ps := range knownPartialSigs {
|
||||||
|
_, err = session.CombineSig(ps)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to combine partial signature: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have the signature
|
||||||
|
evt.Sig = hex.EncodeToString(session.FinalSig().Serialize())
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printPublicCommandForNextPeer(
|
||||||
|
evt *nostr.Event,
|
||||||
|
numSigners int,
|
||||||
|
knownSigners []*btcec.PublicKey,
|
||||||
|
knownNonces [][66]byte,
|
||||||
|
knownPartialSigs []*musig2.PartialSignature,
|
||||||
|
includeNonceSecret bool,
|
||||||
|
) {
|
||||||
|
maybeNonceSecret := ""
|
||||||
|
if includeNonceSecret {
|
||||||
|
maybeNonceSecret = " --musig-nonce-secret '<insert-nonce-secret>'"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "the next signer and they should call this on their side:\nnak event --sec <insert-secret-key> --musig %d %s%s%s%s%s\n",
|
||||||
|
numSigners,
|
||||||
|
eventToCliArgs(evt),
|
||||||
|
signersToCliArgs(knownSigners),
|
||||||
|
noncesToCliArgs(knownNonces),
|
||||||
|
partialSigsToCliArgs(knownPartialSigs),
|
||||||
|
maybeNonceSecret,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func eventToCliArgs(evt *nostr.Event) string {
|
||||||
|
b := strings.Builder{}
|
||||||
|
b.Grow(100)
|
||||||
|
|
||||||
|
b.WriteString("-k ")
|
||||||
|
b.WriteString(strconv.Itoa(evt.Kind))
|
||||||
|
|
||||||
|
b.WriteString(" -ts ")
|
||||||
|
b.WriteString(strconv.FormatInt(int64(evt.CreatedAt), 10))
|
||||||
|
|
||||||
|
b.WriteString(" -c '")
|
||||||
|
b.WriteString(evt.Content)
|
||||||
|
b.WriteString("'")
|
||||||
|
|
||||||
|
for _, tag := range evt.Tags {
|
||||||
|
b.WriteString(" -t '")
|
||||||
|
b.WriteString(tag.Key())
|
||||||
|
if len(tag) > 1 {
|
||||||
|
b.WriteString("=")
|
||||||
|
b.WriteString(tag[1])
|
||||||
|
if len(tag) > 2 {
|
||||||
|
for _, item := range tag[2:] {
|
||||||
|
b.WriteString(";")
|
||||||
|
b.WriteString(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString("'")
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func signersToCliArgs(knownSigners []*btcec.PublicKey) string {
|
||||||
|
b := strings.Builder{}
|
||||||
|
b.Grow(len(knownSigners) * (16 + 66))
|
||||||
|
|
||||||
|
for _, signerPub := range knownSigners {
|
||||||
|
b.WriteString(" --musig-pubkey ")
|
||||||
|
b.WriteString(hex.EncodeToString(signerPub.SerializeCompressed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func noncesToCliArgs(knownNonces [][66]byte) string {
|
||||||
|
b := strings.Builder{}
|
||||||
|
b.Grow(len(knownNonces) * (15 + 132))
|
||||||
|
|
||||||
|
for _, nonce := range knownNonces {
|
||||||
|
b.WriteString(" --musig-nonce ")
|
||||||
|
b.WriteString(hex.EncodeToString(nonce[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func partialSigsToCliArgs(knownPartialSigs []*musig2.PartialSignature) string {
|
||||||
|
b := strings.Builder{}
|
||||||
|
b.Grow(len(knownPartialSigs) * (17 + 64))
|
||||||
|
|
||||||
|
for _, partialSig := range knownPartialSigs {
|
||||||
|
b.WriteString(" --musig-partial ")
|
||||||
|
w := &bytes.Buffer{}
|
||||||
|
partialSig.Encode(w)
|
||||||
|
b.Write([]byte(hex.EncodeToString(w.Bytes())))
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// this function is copied from btcec because it's not exported for some reason
|
||||||
|
func secNonceToPubNonce(secNonce [musig2.SecNonceSize]byte) [musig2.PubNonceSize]byte {
|
||||||
|
var k1Mod, k2Mod btcec.ModNScalar
|
||||||
|
k1Mod.SetByteSlice(secNonce[:btcec.PrivKeyBytesLen])
|
||||||
|
k2Mod.SetByteSlice(secNonce[btcec.PrivKeyBytesLen:])
|
||||||
|
|
||||||
|
var r1, r2 btcec.JacobianPoint
|
||||||
|
btcec.ScalarBaseMultNonConst(&k1Mod, &r1)
|
||||||
|
btcec.ScalarBaseMultNonConst(&k2Mod, &r2)
|
||||||
|
|
||||||
|
// Next, we'll convert the key in jacobian format to a normal public
|
||||||
|
// key expressed in affine coordinates.
|
||||||
|
r1.ToAffine()
|
||||||
|
r2.ToAffine()
|
||||||
|
r1Pub := btcec.NewPublicKey(&r1.X, &r1.Y)
|
||||||
|
r2Pub := btcec.NewPublicKey(&r2.X, &r2.Y)
|
||||||
|
|
||||||
|
var pubNonce [musig2.PubNonceSize]byte
|
||||||
|
|
||||||
|
// The public nonces are serialized as: R1 || R2, where both keys are
|
||||||
|
// serialized in compressed format.
|
||||||
|
copy(pubNonce[:], r1Pub.SerializeCompressed())
|
||||||
|
copy(
|
||||||
|
pubNonce[btcec.PubKeyBytesLenCompressed:],
|
||||||
|
r2Pub.SerializeCompressed(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return pubNonce
|
||||||
|
}
|
||||||
207
relay.go
207
relay.go
@@ -1,12 +1,21 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/nbd-wtf/go-nostr/nip11"
|
"github.com/nbd-wtf/go-nostr/nip11"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/nbd-wtf/go-nostr/nip86"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var relay = &cli.Command{
|
var relay = &cli.Command{
|
||||||
@@ -15,23 +24,203 @@ var relay = &cli.Command{
|
|||||||
Description: `example:
|
Description: `example:
|
||||||
nak relay nostr.wine`,
|
nak relay nostr.wine`,
|
||||||
ArgsUsage: "<relay-url>",
|
ArgsUsage: "<relay-url>",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
url := c.Args().First()
|
for url := range getStdinLinesOrArguments(c.Args()) {
|
||||||
if url == "" {
|
if url == "" {
|
||||||
return fmt.Errorf("specify the <relay-url>")
|
return fmt.Errorf("specify the <relay-url>")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(url, "wss://") && !strings.HasPrefix(url, "ws://") {
|
info, err := nip11.Fetch(ctx, url)
|
||||||
url = "wss://" + url
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err := nip11.Fetch(c.Context, url)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch '%s' information document: %w", url, err)
|
ctx = lineProcessingError(ctx, "failed to fetch '%s' information document: %w", url, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
pretty, _ := json.MarshalIndent(info, "", " ")
|
pretty, _ := json.MarshalIndent(info, "", " ")
|
||||||
stdout(string(pretty))
|
stdout(string(pretty))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
Commands: (func() []*cli.Command {
|
||||||
|
commands := make([]*cli.Command, 0, 12)
|
||||||
|
|
||||||
|
for _, def := range []struct {
|
||||||
|
method string
|
||||||
|
args []string
|
||||||
|
}{
|
||||||
|
{"allowpubkey", []string{"pubkey", "reason"}},
|
||||||
|
{"banpubkey", []string{"pubkey", "reason"}},
|
||||||
|
{"listallowedpubkeys", nil},
|
||||||
|
{"allowpubkey", []string{"pubkey", "reason"}},
|
||||||
|
{"listallowedpubkeys", nil},
|
||||||
|
{"listeventsneedingmoderation", nil},
|
||||||
|
{"allowevent", []string{"id", "reason"}},
|
||||||
|
{"banevent", []string{"id", "reason"}},
|
||||||
|
{"listbannedevents", nil},
|
||||||
|
{"changerelayname", []string{"name"}},
|
||||||
|
{"changerelaydescription", []string{"description"}},
|
||||||
|
{"changerelayicon", []string{"icon"}},
|
||||||
|
{"allowkind", []string{"kind"}},
|
||||||
|
{"disallowkind", []string{"kind"}},
|
||||||
|
{"listallowedkinds", nil},
|
||||||
|
{"blockip", []string{"ip", "reason"}},
|
||||||
|
{"unblockip", []string{"ip", "reason"}},
|
||||||
|
{"listblockedips", nil},
|
||||||
|
} {
|
||||||
|
def := def
|
||||||
|
|
||||||
|
flags := make([]cli.Flag, len(def.args), len(def.args)+4)
|
||||||
|
for i, argName := range def.args {
|
||||||
|
flags[i] = declareFlag(argName)
|
||||||
|
}
|
||||||
|
|
||||||
|
flags = append(flags,
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "sec",
|
||||||
|
Usage: "secret key to sign the event, as nsec, ncryptsec or hex",
|
||||||
|
DefaultText: "the key '1'",
|
||||||
|
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "prompt-sec",
|
||||||
|
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "connect",
|
||||||
|
Usage: "sign event using NIP-46, expects a bunker://... URL",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "connect-as",
|
||||||
|
Usage: "private key to when communicating with the bunker given on --connect",
|
||||||
|
DefaultText: "a random key",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := &cli.Command{
|
||||||
|
Name: def.method,
|
||||||
|
Usage: fmt.Sprintf(`the "%s" relay management RPC call`, def.method),
|
||||||
|
Description: fmt.Sprintf(
|
||||||
|
`the "%s" management RPC call, see https://nips.nostr.com/86 for more information`, def.method),
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
params := make([]any, len(def.args))
|
||||||
|
for i, argName := range def.args {
|
||||||
|
params[i] = getArgument(c, argName)
|
||||||
|
}
|
||||||
|
req := nip86.Request{Method: def.method, Params: params}
|
||||||
|
reqj, _ := json.Marshal(req)
|
||||||
|
|
||||||
|
relayUrls := c.Args().Slice()
|
||||||
|
if len(relayUrls) == 0 {
|
||||||
|
stdout(string(reqj))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, relayUrl := range relayUrls {
|
||||||
|
httpUrl := "http" + nostr.NormalizeURL(relayUrl)[2:]
|
||||||
|
log("calling '%s' on %s... ", def.method, httpUrl)
|
||||||
|
body := bytes.NewBuffer(nil)
|
||||||
|
body.Write(reqj)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", httpUrl, body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
hostname := strings.Split(strings.Split(httpUrl, "://")[1], "/")[0]
|
||||||
|
payloadHash := sha256.Sum256(reqj)
|
||||||
|
authEvent := nostr.Event{
|
||||||
|
Kind: 27235,
|
||||||
|
CreatedAt: nostr.Now(),
|
||||||
|
Tags: nostr.Tags{
|
||||||
|
{"host", hostname},
|
||||||
|
{"payload", hex.EncodeToString(payloadHash[:])},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if bunker != nil {
|
||||||
|
if err := bunker.SignEvent(ctx, &authEvent); err != nil {
|
||||||
|
return fmt.Errorf("failed to sign with bunker: %w", err)
|
||||||
|
}
|
||||||
|
} else if err := authEvent.Sign(sec); err != nil {
|
||||||
|
return fmt.Errorf("error signing with provided key: %w", err)
|
||||||
|
}
|
||||||
|
evtj, _ := json.Marshal(authEvent)
|
||||||
|
req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj))
|
||||||
|
|
||||||
|
// Content-Type
|
||||||
|
req.Header.Set("Content-Type", "application/nostr+json+rpc")
|
||||||
|
|
||||||
|
// make request to relay
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log("failed: %s\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
log("failed to read response: %s\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 300 {
|
||||||
|
log("failed with status %d\n", resp.StatusCode)
|
||||||
|
bodyPrintable := string(b)
|
||||||
|
if len(bodyPrintable) > 300 {
|
||||||
|
bodyPrintable = bodyPrintable[0:297] + "..."
|
||||||
|
}
|
||||||
|
log(bodyPrintable)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var response nip86.Response
|
||||||
|
if err := json.Unmarshal(b, &response); err != nil {
|
||||||
|
log("bad json response: %s\n", err)
|
||||||
|
bodyPrintable := string(b)
|
||||||
|
if len(bodyPrintable) > 300 {
|
||||||
|
bodyPrintable = bodyPrintable[0:297] + "..."
|
||||||
|
}
|
||||||
|
log(bodyPrintable)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
// print the result
|
||||||
|
log("\n")
|
||||||
|
pretty, _ := json.MarshalIndent(response, "", " ")
|
||||||
|
stdout(string(pretty))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Flags: flags,
|
||||||
|
}
|
||||||
|
|
||||||
|
commands = append(commands, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands
|
||||||
|
})(),
|
||||||
|
}
|
||||||
|
|
||||||
|
func declareFlag(argName string) cli.Flag {
|
||||||
|
usage := "parameter for this management RPC call, see https://nips.nostr.com/86 for more information."
|
||||||
|
switch argName {
|
||||||
|
case "kind":
|
||||||
|
return &cli.IntFlag{Name: argName, Required: true, Usage: usage}
|
||||||
|
case "reason":
|
||||||
|
return &cli.StringFlag{Name: argName, Usage: usage}
|
||||||
|
default:
|
||||||
|
return &cli.StringFlag{Name: argName, Required: true, Usage: usage}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getArgument(c *cli.Command, argName string) any {
|
||||||
|
switch argName {
|
||||||
|
case "kind":
|
||||||
|
return c.Int(argName)
|
||||||
|
default:
|
||||||
|
return c.String(argName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
req.go
37
req.go
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -9,7 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/mailru/easyjson"
|
"github.com/mailru/easyjson"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const CATEGORY_FILTER_ATTRIBUTES = "FILTER ATTRIBUTES"
|
const CATEGORY_FILTER_ATTRIBUTES = "FILTER ATTRIBUTES"
|
||||||
@@ -103,6 +104,11 @@ example:
|
|||||||
Name: "auth",
|
Name: "auth",
|
||||||
Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "force-pre-auth",
|
||||||
|
Aliases: []string{"fpa"},
|
||||||
|
Usage: "after connecting, for a NIP-42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"",
|
||||||
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "sec",
|
Name: "sec",
|
||||||
Usage: "secret key to sign the AUTH challenge, as hex or nsec",
|
Usage: "secret key to sign the AUTH challenge, as hex or nsec",
|
||||||
@@ -124,33 +130,34 @@ example:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
ArgsUsage: "[relay...]",
|
ArgsUsage: "[relay...]",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
var pool *nostr.SimplePool
|
var pool *nostr.SimplePool
|
||||||
|
|
||||||
relayUrls := c.Args().Slice()
|
relayUrls := c.Args().Slice()
|
||||||
if len(relayUrls) > 0 {
|
if len(relayUrls) > 0 {
|
||||||
var relays []*nostr.Relay
|
var relays []*nostr.Relay
|
||||||
pool, relays = connectToAllRelays(c.Context, relayUrls, nostr.WithAuthHandler(func(evt *nostr.Event) error {
|
pool, relays = connectToAllRelays(ctx, relayUrls, c.Bool("force-pre-auth"), nostr.WithAuthHandler(func(evt *nostr.Event) error {
|
||||||
if !c.Bool("auth") {
|
if !c.Bool("auth") && !c.Bool("force-pre-auth") {
|
||||||
return fmt.Errorf("auth not authorized")
|
return fmt.Errorf("auth not authorized")
|
||||||
}
|
}
|
||||||
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(c)
|
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var pk string
|
var pk string
|
||||||
if bunker != nil {
|
if bunker != nil {
|
||||||
pk, err = bunker.GetPublicKey(c.Context)
|
pk, err = bunker.GetPublicKey(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get public key from bunker: %w", err)
|
return fmt.Errorf("failed to get public key from bunker: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pk, _ = nostr.GetPublicKey(sec)
|
pk, _ = nostr.GetPublicKey(sec)
|
||||||
}
|
}
|
||||||
log("performing auth as %s...\n", pk)
|
log("performing auth as %s... ", pk)
|
||||||
|
|
||||||
if bunker != nil {
|
if bunker != nil {
|
||||||
return bunker.SignEvent(c.Context, evt)
|
return bunker.SignEvent(ctx, evt)
|
||||||
} else {
|
} else {
|
||||||
return evt.Sign(sec)
|
return evt.Sign(sec)
|
||||||
}
|
}
|
||||||
@@ -175,7 +182,7 @@ example:
|
|||||||
filter := nostr.Filter{}
|
filter := nostr.Filter{}
|
||||||
if stdinFilter != "" {
|
if stdinFilter != "" {
|
||||||
if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil {
|
if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil {
|
||||||
lineProcessingError(c, "invalid filter '%s' received from stdin: %s", stdinFilter, err)
|
ctx = lineProcessingError(ctx, "invalid filter '%s' received from stdin: %s", stdinFilter, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,8 +193,8 @@ example:
|
|||||||
if ids := c.StringSlice("id"); len(ids) > 0 {
|
if ids := c.StringSlice("id"); len(ids) > 0 {
|
||||||
filter.IDs = append(filter.IDs, ids...)
|
filter.IDs = append(filter.IDs, ids...)
|
||||||
}
|
}
|
||||||
if kinds := c.IntSlice("kind"); len(kinds) > 0 {
|
for _, kind64 := range c.IntSlice("kind") {
|
||||||
filter.Kinds = append(filter.Kinds, kinds...)
|
filter.Kinds = append(filter.Kinds, int(kind64))
|
||||||
}
|
}
|
||||||
if search := c.String("search"); search != "" {
|
if search := c.String("search"); search != "" {
|
||||||
filter.Search = search
|
filter.Search = search
|
||||||
@@ -245,7 +252,9 @@ example:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if limit := c.Int("limit"); limit != 0 {
|
if limit := c.Int("limit"); limit != 0 {
|
||||||
filter.Limit = limit
|
filter.Limit = int(limit)
|
||||||
|
} else if c.IsSet("limit") || c.Bool("stream") {
|
||||||
|
filter.LimitZero = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(relayUrls) > 0 {
|
if len(relayUrls) > 0 {
|
||||||
@@ -253,7 +262,7 @@ example:
|
|||||||
if c.Bool("stream") {
|
if c.Bool("stream") {
|
||||||
fn = pool.SubMany
|
fn = pool.SubMany
|
||||||
}
|
}
|
||||||
for ie := range fn(c.Context, relayUrls, nostr.Filters{filter}) {
|
for ie := range fn(ctx, relayUrls, nostr.Filters{filter}) {
|
||||||
stdout(ie.Event)
|
stdout(ie.Event)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -270,7 +279,7 @@ example:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(c)
|
exitIfLineProcessingError(ctx)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
15
verify.go
15
verify.go
@@ -1,10 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var verify = &cli.Command{
|
var verify = &cli.Command{
|
||||||
@@ -14,28 +15,28 @@ var verify = &cli.Command{
|
|||||||
echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak verify
|
echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak verify
|
||||||
|
|
||||||
it outputs nothing if the verification is successful.`,
|
it outputs nothing if the verification is successful.`,
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for stdinEvent := range getStdinLinesOrFirstArgument(c.Args().First()) {
|
for stdinEvent := range getStdinLinesOrArguments(c.Args()) {
|
||||||
evt := nostr.Event{}
|
evt := nostr.Event{}
|
||||||
if stdinEvent != "" {
|
if stdinEvent != "" {
|
||||||
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
||||||
lineProcessingError(c, "invalid event: %s", err)
|
ctx = lineProcessingError(ctx, "invalid event: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if evt.GetID() != evt.ID {
|
if evt.GetID() != evt.ID {
|
||||||
lineProcessingError(c, "invalid .id, expected %s, got %s", evt.GetID(), evt.ID)
|
ctx = lineProcessingError(ctx, "invalid .id, expected %s, got %s", evt.GetID(), evt.ID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err := evt.CheckSignature(); !ok {
|
if ok, err := evt.CheckSignature(); !ok {
|
||||||
lineProcessingError(c, "invalid signature: %s", err)
|
ctx = lineProcessingError(ctx, "invalid signature: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(c)
|
exitIfLineProcessingError(ctx)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user