Compare commits

...

48 Commits

Author SHA1 Message Date
fiatjaf
eccce6dc4a nak key combine now returns all possible combinations. 2024-05-16 19:50:52 -03:00
fiatjaf
31f007ffc2 fix: musig2 event to cli args was generating multivalue tags wrongly. 2024-05-15 18:49:22 -03:00
fiatjaf
bb45059218 refactor bunker to work better. remove prompts, use lists of keys and secrets and a new random key. 2024-05-15 17:31:01 -03:00
fiatjaf
71dfe583ed rename flags from --musig2-... to --musig-..., add id to event and other small tweaks. 2024-05-14 23:52:56 -03:00
fiatjaf
84bde7dacd musig2 works now. 2024-05-14 23:41:12 -03:00
fiatjaf
81968f6c0c nak key combine and nak event --musig2 2024-05-14 15:23:08 -03:00
fiatjaf
f198a46c19 remove wss:// from relay urls in readme. 2024-04-29 08:36:59 -03:00
fiatjaf
c3ea9c15f6 LimitZero when -l 0 and when --stream 2024-03-29 08:16:28 -03:00
Yasuhiro Matsumoto
8ddb9ce021 fix color output on Windows 2024-03-28 15:52:30 -03:00
fiatjaf
569d38a137 accept multiple arguments in many commands, add a lot of more tests. 2024-03-19 11:34:59 -03:00
fiatjaf
34c189af28 bunker improvements. 2024-03-02 08:18:40 -03:00
fiatjaf
ffe2db7f96 event: accept tags with a single item. 2024-02-21 09:47:34 -03:00
fiatjaf
c5f7926471 bunker: repeat connection info every now and then. 2024-02-17 17:56:57 -03:00
fiatjaf
e008e08105 bunker: send responses to relays concurrently. 2024-02-16 11:08:48 -03:00
fiatjaf
5dd5a7c699 bunker: better colors and prompts. 2024-02-12 15:39:13 -03:00
fiatjaf
347a82eaa9 verify: accept event to be verified as json argument. 2024-02-12 15:38:43 -03:00
fiatjaf
e89823b10e ensure at least one blank line will be emitted when piped. 2024-02-06 12:47:58 -03:00
fiatjaf
6626001dd2 --connect-as to specify client pubkey when using --connect to bunker 2024-02-06 12:47:46 -03:00
fiatjaf
b7a7e0504f --connect to use nip46 as a client to sign event and auth messages. 2024-02-06 00:58:26 -03:00
mattn
01e1f52a70 fix panic (#12)
* c.Args().Len() - 1 might be negative value

* fix getStdinLinesOrFirstArgument

* fix getStdinLinesOrFirstArgument
2024-02-03 10:03:32 -03:00
fiatjaf
0b9e861f90 fix: print prompt to stderr. 2024-02-02 14:13:11 -03:00
fiatjaf
bda18e035a fix reading hex secret key from input. 2024-02-02 14:11:58 -03:00
fiatjaf
0d46d48881 fix naddr. 2024-01-29 15:37:21 -03:00
fiatjaf
6f24112b5e support ncryptsec in all operations that require a private key and have a nice password prompt. 2024-01-29 10:56:58 -03:00
fiatjaf
f4921f1fe9 nak key: generate, public, encrypt, decrypt. 2024-01-25 08:21:09 -03:00
fiatjaf
3dfcec69b7 --nevent flag on nak event to print an nevent at the end. 2024-01-24 22:43:23 -03:00
fiatjaf
14b69f36cf -q to silence stderr, -qq to silence everything. 2024-01-24 22:38:51 -03:00
fiatjaf
3f7089e27e signal that we accept patches over NIP-34. 2024-01-23 10:20:54 -03:00
fiatjaf
6a75c8aec3 UseShortOptionHandling and Suggest 2024-01-23 10:20:54 -03:00
fiatjaf
b17887fe21 replace validate32BytesHex() with native calls from go-nostr. 2024-01-21 07:45:22 -03:00
fiatjaf
77103cae0c req: -d flag too. 2024-01-17 08:50:59 -03:00
fiatjaf
59a2c16b42 event: -d shortcut flag and use .AppendUnique() 2024-01-17 08:49:18 -03:00
fiatjaf
48d19196bb fix publishing to multiple relays. 2024-01-16 09:16:56 -03:00
fiatjaf
ad7010e506 use scanner.Buffer() to make stdin able to parse hellish giant events up to 256kb (the default was 64kb). 2024-01-11 21:48:54 -03:00
fiatjaf
584881266e bunker: update to go-nostr nip46 api breaking change. 2024-01-11 21:41:50 -03:00
fiatjaf
a30f422d7d close relay websockets cleanly. 2024-01-11 21:29:46 -03:00
fiatjaf
637b9440ef upgrade go-nostr and xsync. 2024-01-10 21:19:19 -03:00
fiatjaf
16c1e795bd fetch: more places to fetch relay lists from. 2024-01-02 11:05:43 -03:00
OHASHI Hideya
8373da647e Fix tags with values containing = 2023-12-24 09:29:29 -03:00
fiatjaf
f295f130f2 remove .scalafmt.conf 2023-12-23 21:30:58 -03:00
fiatjaf
5415fd369c update go-nostr to fix pool infinite loop. 2023-12-15 11:15:12 -03:00
Daniel Cadenas
f35cb4bd1d Fix tags with values containing = 2023-12-13 20:48:01 -03:00
Yasuhiro Matsumoto
242b028656 -until now 2023-12-12 13:34:30 -03:00
Yasuhiro Matsumoto
f0d90b567c -since now 2023-12-12 13:34:30 -03:00
fiatjaf
2d1e27f766 fix for nil error case on publish. 2023-12-10 20:49:08 -03:00
fiatjaf
bfa72640cd bunker: a better prompt. 2023-12-09 17:42:01 -03:00
fiatjaf
e5b0b15908 bunker: improve error message. 2023-12-09 17:11:55 -03:00
fiatjaf
0860cfcf6d rename nsecbunker->bunker. 2023-12-09 16:37:59 -03:00
19 changed files with 1368 additions and 327 deletions

View File

@@ -1,2 +0,0 @@
version = 3.5.8
runner.dialect = scala3

View File

@@ -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,5 +79,9 @@ 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
Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`.

225
bunker.go Normal file
View File

@@ -0,0 +1,225 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/url"
"os"
"strings"
"sync"
"time"
"github.com/fatih/color"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip46"
"github.com/urfave/cli/v2"
"golang.org/x/exp/slices"
)
var bunker = &cli.Command{
Name: "bunker",
Usage: "starts a NIP-46 signer daemon with the given --sec key",
ArgsUsage: "[relay...]",
Description: ``,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "sec",
Usage: "secret key to sign the event, as hex or nsec",
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.StringSliceFlag{
Name: "authorized-secrets",
Aliases: []string{"s"},
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 {
// try to connect to the relays here
qs := url.Values{}
relayURLs := make([]string, 0, c.Args().Len())
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
_, relays := connectToAllRelays(c.Context, relayUrls)
if len(relays) == 0 {
log("failed to connect to any of the given relays.\n")
os.Exit(3)
}
for _, relay := range relays {
relayURLs = append(relayURLs, relay.URL)
qs.Add("relay", relay.URL)
}
}
if len(relayURLs) == 0 {
return fmt.Errorf("not connected to any relays: please specify at least one")
}
// gather the secret key
sec, _, err := gatherSecretKeyOrBunkerFromArguments(c)
if err != nil {
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)
if err != nil {
return err
}
npub, _ := nip19.EncodePublicKey(pubkey)
bold := color.New(color.Bold).Sprint
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
}
restartCommand := fmt.Sprintf("nak bunker %s%s %s",
secretKeyFlag,
preauthorizedFlags,
strings.Join(relayURLs, " "),
)
log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n\n",
bold(relayURLs),
bold(pubkey),
bold(npub),
authorizedKeysStr,
authorizedSecretsStr,
color.CyanString(restartCommand),
bold(bunkerURI),
)
}
printBunkerInfo()
// subscribe to relays
pool := nostr.NewSimplePool(c.Context)
now := nostr.Now()
events := pool.SubMany(c.Context, relayURLs, nostr.Filters{
{
Kinds: []int{nostr.KindNostrConnect},
Tags: nostr.TagMap{"p": []string{pubkey}},
Since: &now,
LimitZero: true,
},
})
signer := nip46.NewStaticKeySigner(sec)
handlerWg := sync.WaitGroup{}
printLock := sync.Mutex{}
// just a gimmick
var cancelPreviousBunkerInfoPrint context.CancelFunc
_, cancel := context.WithCancel(c.Context)
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 {
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 {
log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error())
continue
}
jreq, _ := json.MarshalIndent(req, " ", " ")
log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey), string(jreq))
jresp, _ := json.MarshalIndent(resp, " ", " ")
log("~ responding with %s\n", string(jresp))
handlerWg.Add(len(relayURLs))
for _, relayURL := range relayURLs {
go func(relayURL string) {
if relay, _ := pool.EnsureRelay(relayURL); relay != nil {
err := relay.Publish(c.Context, eventResponse)
printLock.Lock()
if err == nil {
log("* sent response through %s\n", relay.URL)
} else {
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(c.Context)
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
},
}

View File

@@ -78,7 +78,7 @@ var count = &cli.Command{
tags := make([][]string, 0, 5) tags := make([][]string, 0, 5)
for _, tagFlag := range c.StringSlice("tag") { for _, tagFlag := range c.StringSlice("tag") {
spl := strings.Split(tagFlag, "=") spl := strings.SplitN(tagFlag, "=", 2)
if len(spl) == 2 && len(spl[0]) == 1 { if len(spl) == 2 && len(spl[0]) == 1 {
tags = append(tags, spl) tags = append(tags, spl)
} else { } else {
@@ -139,7 +139,7 @@ var count = &cli.Command{
var result string var result string
j, _ := json.Marshal([]any{"COUNT", "nak", filter}) j, _ := json.Marshal([]any{"COUNT", "nak", filter})
result = string(j) result = string(j)
fmt.Println(result) stdout(result)
} }
return nil return nil

View File

@@ -3,7 +3,6 @@ package main
import ( import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt"
"strings" "strings"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
@@ -34,7 +33,7 @@ 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(c *cli.Context) error {
for input := range getStdinLinesOrFirstArgument(c) { for input := range getStdinLinesOrArguments(c.Args()) {
if strings.HasPrefix(input, "nostr:") { if strings.HasPrefix(input, "nostr:") {
input = input[6:] input = input[6:]
} }
@@ -68,7 +67,7 @@ var decode = &cli.Command{
continue continue
} }
fmt.Println(decodeResult.JSON()) stdout(decodeResult.JSON())
} }

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"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/v2"
) )
@@ -28,14 +29,14 @@ var encode = &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(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c) { for target := range getStdinLinesOrArguments(c.Args()) {
if err := validate32BytesHex(target); err != nil { if ok := nostr.IsValidPublicKey(target); !ok {
lineProcessingError(c, "invalid public key: %s", target, err) lineProcessingError(c, "invalid public key: %s", target)
continue continue
} }
if npub, err := nip19.EncodePublicKey(target); err == nil { if npub, err := nip19.EncodePublicKey(target); err == nil {
fmt.Println(npub) stdout(npub)
} else { } else {
return err return err
} }
@@ -49,14 +50,14 @@ var encode = &cli.Command{
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(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c) { for target := range getStdinLinesOrArguments(c.Args()) {
if err := validate32BytesHex(target); err != nil { if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid private key: %s", target, err) lineProcessingError(c, "invalid private key: %s", target)
continue continue
} }
if npub, err := nip19.EncodePrivateKey(target); err == nil { if npub, err := nip19.EncodePrivateKey(target); err == nil {
fmt.Println(npub) stdout(npub)
} else { } else {
return err return err
} }
@@ -77,9 +78,9 @@ var encode = &cli.Command{
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c) { for target := range getStdinLinesOrArguments(c.Args()) {
if err := validate32BytesHex(target); err != nil { if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid public key: %s", target, err) lineProcessingError(c, "invalid public key: %s", target)
continue continue
} }
@@ -89,7 +90,7 @@ var encode = &cli.Command{
} }
if npub, err := nip19.EncodeProfile(target, relays); err == nil { if npub, err := nip19.EncodeProfile(target, relays); err == nil {
fmt.Println(npub) stdout(npub)
} else { } else {
return err return err
} }
@@ -114,16 +115,16 @@ var encode = &cli.Command{
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c) { for target := range getStdinLinesOrArguments(c.Args()) {
if err := validate32BytesHex(target); err != nil { if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid event id: %s", target, err) lineProcessingError(c, "invalid event id: %s", target)
continue continue
} }
author := c.String("author") author := c.String("author")
if author != "" { if author != "" {
if err := validate32BytesHex(author); err != nil { if ok := nostr.IsValidPublicKey(author); !ok {
return err return fmt.Errorf("invalid 'author' public key")
} }
} }
@@ -133,7 +134,7 @@ var encode = &cli.Command{
} }
if npub, err := nip19.EncodeEvent(target, relays, author); err == nil { if npub, err := nip19.EncodeEvent(target, relays, author); err == nil {
fmt.Println(npub) stdout(npub)
} else { } else {
return err return err
} }
@@ -174,8 +175,8 @@ var encode = &cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
for d := range getStdinLinesOrBlank() { for d := range getStdinLinesOrBlank() {
pubkey := c.String("pubkey") pubkey := c.String("pubkey")
if err := validate32BytesHex(pubkey); err != nil { if ok := nostr.IsValidPublicKey(pubkey); !ok {
return err return fmt.Errorf("invalid 'pubkey'")
} }
kind := c.Int("kind") kind := c.Int("kind")
@@ -197,7 +198,7 @@ var encode = &cli.Command{
} }
if npub, err := nip19.EncodeEntity(pubkey, kind, d, relays); err == nil { if npub, err := nip19.EncodeEntity(pubkey, kind, d, relays); err == nil {
fmt.Println(npub) stdout(npub)
} else { } else {
return err return err
} }
@@ -211,14 +212,14 @@ var encode = &cli.Command{
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(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c) { for target := range getStdinLinesOrArguments(c.Args()) {
if err := validate32BytesHex(target); err != nil { if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid event id: %s", target, err) lineProcessingError(c, "invalid event id: %s", target)
continue continue
} }
if note, err := nip19.EncodeNote(target); err == nil { if note, err := nip19.EncodeNote(target); err == nil {
fmt.Println(note) stdout(note)
} else { } else {
return err return err
} }

129
event.go
View File

@@ -11,6 +11,7 @@ import (
"github.com/mailru/easyjson" "github.com/mailru/easyjson"
"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/nson" "github.com/nbd-wtf/go-nostr/nson"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
@@ -43,6 +44,40 @@ example:
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.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",
},
// ~ 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",
@@ -51,6 +86,10 @@ 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: "nevent",
Usage: "print the nevent code (to stderr) after the event is published",
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "nson", Name: "nson",
Usage: "encode the event using NSON", Usage: "encode the event using NSON",
@@ -87,6 +126,11 @@ example:
Usage: "shortcut for --tag p=<value>", Usage: "shortcut for --tag p=<value>",
Category: CATEGORY_EVENT_FIELDS, Category: CATEGORY_EVENT_FIELDS,
}, },
&cli.StringSliceFlag{
Name: "d",
Usage: "shortcut for --tag d=<value>",
Category: CATEGORY_EVENT_FIELDS,
},
&cli.StringFlag{ &cli.StringFlag{
Name: "created-at", Name: "created-at",
Aliases: []string{"time", "ts"}, Aliases: []string{"time", "ts"},
@@ -108,8 +152,13 @@ example:
} }
} }
// gather the secret key defer func() {
sec, err := gatherSecretKeyFromArguments(c) for _, relay := range relays {
relay.Close()
}
}()
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(c)
if err != nil { if err != nil {
return err return err
} }
@@ -117,7 +166,6 @@ example:
doAuth := c.Bool("auth") doAuth := c.Bool("auth")
// then process input and generate events // then process input and generate events
nextline:
for stdinEvent := range getStdinLinesOrBlank() { for stdinEvent := range getStdinLinesOrBlank() {
evt := nostr.Event{ evt := nostr.Event{
Tags: make(nostr.Tags, 0, 3), Tags: make(nostr.Tags, 0, 3),
@@ -153,22 +201,26 @@ example:
tags := make(nostr.Tags, 0, 5) tags := make(nostr.Tags, 0, 5)
for _, tagFlag := range c.StringSlice("tag") { for _, tagFlag := range c.StringSlice("tag") {
// tags are in the format key=value // tags are in the format key=value
spl := strings.Split(tagFlag, "=") tagName, tagValue, found := strings.Cut(tagFlag, "=")
if len(spl) == 2 && len(spl[0]) > 0 { tag := []string{tagName}
tag := nostr.Tag{spl[0]} if found {
// tags may also contain extra elements separated with a ";" // tags may also contain extra elements separated with a ";"
spl2 := strings.Split(spl[1], ";") tagValues := strings.Split(tagValue, ";")
tag = append(tag, spl2...) tag = append(tag, tagValues...)
// ~
tags = append(tags, tag)
} }
tags = tags.AppendUnique(tag)
} }
for _, etag := range c.StringSlice("e") { for _, etag := range c.StringSlice("e") {
tags = append(tags, []string{"e", etag}) tags = tags.AppendUnique([]string{"e", etag})
mustRehashAndResign = true mustRehashAndResign = true
} }
for _, ptag := range c.StringSlice("p") { for _, ptag := range c.StringSlice("p") {
tags = append(tags, []string{"p", ptag}) tags = tags.AppendUnique([]string{"p", ptag})
mustRehashAndResign = true
}
for _, dtag := range c.StringSlice("d") {
tags = tags.AppendUnique([]string{"d", dtag})
mustRehashAndResign = true mustRehashAndResign = true
} }
if len(tags) > 0 { if len(tags) > 0 {
@@ -195,7 +247,26 @@ example:
} }
if evt.Sig == "" || mustRehashAndResign { if evt.Sig == "" || mustRehashAndResign {
if err := evt.Sign(sec); err != nil { if bunker != nil {
if err := bunker.SignEvent(c.Context, &evt); err != nil {
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(c.Context,
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 {
return fmt.Errorf("error signing with provided key: %w", err) return fmt.Errorf("error signing with provided key: %w", err)
} }
} }
@@ -211,9 +282,10 @@ example:
j, _ := easyjson.Marshal(&evt) j, _ := easyjson.Marshal(&evt)
result = string(j) result = string(j)
} }
fmt.Println(result) stdout(result)
// publish to relays // publish to relays
successRelays := make([]string, 0, len(relays))
if len(relays) > 0 { if len(relays) > 0 {
os.Stdout.Sync() os.Stdout.Sync()
for _, relay := range relays { for _, relay := range relays {
@@ -222,18 +294,33 @@ example:
ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) ctx, cancel := context.WithTimeout(c.Context, 10*time.Second)
defer cancel() defer cancel()
if err := relay.Publish(ctx, evt); err == nil { err := relay.Publish(ctx, evt)
if err == nil {
// published fine // published fine
log("success.\n") log("success.\n")
continue nextline successRelays = append(successRelays, relay.URL)
continue // continue to next relay
} }
// error publishing // error publishing
if strings.HasPrefix(err.Error(), "msg: auth-required:") && sec != "" && doAuth { if strings.HasPrefix(err.Error(), "msg: auth-required:") && (sec != "" || bunker != nil) && doAuth {
// 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
pk, _ := nostr.GetPublicKey(sec) var pk string
if bunker != nil {
pk, err = bunker.GetPublicKey(c.Context)
if err != nil {
return fmt.Errorf("failed to get public key from bunker: %w", err)
}
} else {
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 { return evt.Sign(sec) }); err == nil { if err := relay.Auth(c.Context, func(evt *nostr.Event) error {
if bunker != nil {
return bunker.SignEvent(c.Context, evt)
}
return evt.Sign(sec)
}); err == nil {
// try to publish again, but this time don't try to auth again // try to publish again, but this time don't try to auth again
doAuth = false doAuth = false
goto publish goto publish
@@ -243,6 +330,10 @@ example:
} }
log("failed: %s\n", err) log("failed: %s\n", err)
} }
if len(successRelays) > 0 && c.Bool("nevent") {
nevent, _ := nip19.EncodeEvent(evt.ID, successRelays, evt.PubKey)
log(nevent + "\n")
}
} }
} }

View File

@@ -1,15 +1,62 @@
package main package main
import "os"
func ExampleEventBasic() { func ExampleEventBasic() {
app.Run([]string{"nak", "event", "--ts", "1699485669"}) app.Run([]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"} // {"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"}
} }
func ExampleEventComplex() { // (for some reason there can only be one test dealing with stdin in the suite otherwise it halts)
app.Run([]string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def;nothing"}) 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([]string{"nak", "event", "-t", "t=spam", "--ts", "1699485669"})
// Output: // Output:
// {"id":"aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"kind":11,"tags":[["t","spam"],["r","https://abc.def","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"1165ac7a27d774d351ef19c8e918fb22f4005fcba193976c3d7edba6ef87ead7f14467f376a9e199f8371835368d86a8506f591e382528d00287fb168a7b8f38"} // {"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() {
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"})
// 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"}
}
func ExampleEncode() {
app.Run([]string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822", "a5592173975ded9f836a9572ea8b11a7e16ceb66464d66d50b27163f7f039d2c"})
// npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28
// nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug
// nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug
// nprofile1qqs22kfpwwt4mmvlsd4f2uh23vg60ctvadnyvntx659jw93l0upe6tqpz9mhxue69uhk27rpd4cxcefwvdhk64h265a
}
func ExampleDecode() {
app.Run([]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() {
@@ -18,14 +65,71 @@ func ExampleReq() {
// ["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([]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([]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([]string{"nak", "key", "public", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"})
// Output:
// 70f7120d065870513a6bddb61c8d400ad1e43449b1900ffdb5551e4c421375c8
// 718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029
}
func ExampleKeyDecrypt() {
app.Run([]string{"nak", "key", "decrypt", "ncryptsec1qggfep0m5ythsegkmwfrhhx2zx5gazyhdygvlngcds4wsgdpzfy6nr0exy0pdk0ydwrqyhndt2trtwcgwwag0ja3aqclzptfxxqvprdyaz3qfrmazpecx2ff6dph5mfdjnh5sw8sgecul32eru6xet34", "banana"})
// Output:
// nsec180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsgyumg0
}
func ExampleRelay() {
app.Run([]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"
// }
} }

View File

@@ -1,8 +1,6 @@
package main package main
import ( import (
"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"
sdk "github.com/nbd-wtf/nostr-sdk" sdk "github.com/nbd-wtf/nostr-sdk"
@@ -24,7 +22,16 @@ var fetch = &cli.Command{
}, },
ArgsUsage: "[nip19code]", ArgsUsage: "[nip19code]",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
for code := range getStdinLinesOrFirstArgument(c) { pool := nostr.NewSimplePool(c.Context)
defer func() {
pool.Relays.Range(func(_ string, relay *nostr.Relay) bool {
relay.Close()
return true
})
}()
for code := range getStdinLinesOrArguments(c.Args()) {
filter := nostr.Filter{} filter := nostr.Filter{}
prefix, value, err := nip19.Decode(code) prefix, value, err := nip19.Decode(code)
@@ -67,10 +74,10 @@ var fetch = &cli.Command{
authorHint = v authorHint = v
} }
pool := nostr.NewSimplePool(c.Context)
if authorHint != "" { if authorHint != "" {
relayList := sdk.FetchRelaysForPubkey(c.Context, pool, authorHint, relayList := sdk.FetchRelaysForPubkey(c.Context, pool, authorHint,
"wss://purplepag.es", "wss://offchain.pub", "wss://public.relaying.io") "wss://purplepag.es", "wss://relay.damus.io", "wss://relay.noswhere.com",
"wss://nos.lol", "wss://public.relaying.io", "wss://relay.nostr.band")
for _, relayListItem := range relayList { for _, relayListItem := range relayList {
if relayListItem.Outbox { if relayListItem.Outbox {
relays = append(relays, relayListItem.URL) relays = append(relays, relayListItem.URL)
@@ -84,7 +91,7 @@ var fetch = &cli.Command{
} }
for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) { for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) {
fmt.Println(ie.Event) stdout(ie.Event)
} }
} }

32
go.mod
View File

@@ -5,36 +5,38 @@ go 1.21
toolchain go1.21.0 toolchain go1.21.0
require ( require (
github.com/bgentry/speakeasy v0.1.0 github.com/btcsuite/btcd/btcec/v2 v2.3.3
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
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.27.0 github.com/nbd-wtf/go-nostr v0.31.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/v2 v2.25.7
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/cespare/xxhash/v2 v2.2.0 // indirect github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // 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/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // 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/golang/glog v1.1.2 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tidwall/gjson v1.17.0 // indirect github.com/tidwall/gjson v1.17.1 // 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 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/sys v0.14.0 // indirect golang.org/x/crypto v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.8.0 // indirect
) )

74
go.sum
View File

@@ -1,21 +1,19 @@
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
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=
@@ -25,9 +23,12 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
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/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 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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=
@@ -38,16 +39,11 @@ 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/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
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=
@@ -56,11 +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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
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=
@@ -81,8 +74,13 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/nbd-wtf/go-nostr v0.27.0 h1:h6JmMMmfNcAORTL2kk/K3+U6Mju6rk/IjcHA/PMeOc8= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/nbd-wtf/go-nostr v0.27.0/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/nbd-wtf/go-nostr v0.31.2 h1:PkHCAsSzG0Ce8tfF7LKyvZOjYtCdC+hPh5KfO/Rl1b4=
github.com/nbd-wtf/go-nostr v0.31.2/go.mod h1:vHKtHyLXDXzYBN0fi/9Y/Q5AD0p+hk8TQVKlldAi0gI=
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=
@@ -94,22 +92,19 @@ 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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4=
github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= 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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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=
@@ -122,8 +117,10 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr
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/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
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=
@@ -141,13 +138,15 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20221010170243-090e33056c14/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=
@@ -161,7 +160,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/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=

View File

@@ -5,27 +5,30 @@ import (
"context" "context"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"math/rand"
"net/url" "net/url"
"os" "os"
"strings" "strings"
"github.com/bgentry/speakeasy" "github.com/chzyer/readline"
"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/nip49"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
const ( const (
LINE_PROCESSING_ERROR = iota LINE_PROCESSING_ERROR = iota
BOLD_ON = "\033[1m"
BOLD_OFF = "\033[21m"
) )
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
func isPiped() bool { func isPiped() bool {
stat, _ := os.Stdin.Stat() stat, _ := os.Stdin.Stat()
return stat.Mode()&os.ModeCharDevice == 0 return stat.Mode()&os.ModeCharDevice == 0
@@ -43,14 +46,21 @@ func getStdinLinesOrBlank() chan string {
} }
} }
func getStdinLinesOrFirstArgument(c *cli.Context) 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
target := c.Args().First() if len(args) > 0 {
if target != "" { argsCh := make(chan string, 1)
single := make(chan string, 1) go func() {
single <- target for _, arg := range args {
close(single) argsCh <- arg
return single }
close(argsCh)
}()
return argsCh
} }
// try the stdin // try the stdin
@@ -64,8 +74,14 @@ func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) {
// piped // piped
go func() { go func() {
scanner := bufio.NewScanner(os.Stdin) scanner := bufio.NewScanner(os.Stdin)
scanner.Buffer(make([]byte, 16*1024), 256*1024)
hasEmittedAtLeastOne := false
for scanner.Scan() { for scanner.Scan() {
ch <- strings.TrimSpace(scanner.Text()) ch <- strings.TrimSpace(scanner.Text())
hasEmittedAtLeastOne = true
}
if !hasEmittedAtLeastOne {
ch <- ""
} }
close(ch) close(ch)
}() }()
@@ -95,20 +111,6 @@ func validateRelayURLs(wsurls []string) error {
return nil return nil
} }
func validate32BytesHex(target string) error {
if _, err := hex.DecodeString(target); err != nil {
return fmt.Errorf("target '%s' is not valid hex: %s", target, err)
}
if len(target) != 64 {
return fmt.Errorf("expected '%s' to be 64 characters (32 bytes), got %d", target, len(target))
}
if strings.ToLower(target) != target {
return fmt.Errorf("expected target to be all lowercase hex. try again with '%s'", strings.ToLower(target))
}
return nil
}
func connectToAllRelays( func connectToAllRelays(
ctx context.Context, ctx context.Context,
relayUrls []string, relayUrls []string,
@@ -139,32 +141,107 @@ func exitIfLineProcessingError(c *cli.Context) {
} }
} }
func gatherSecretKeyFromArguments(c *cli.Context) (string, error) { func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.BunkerClient, error) {
var err error
if bunkerURL := c.String("connect"); bunkerURL != "" {
clientKey := c.String("connect-as")
if clientKey != "" {
clientKey = strings.Repeat("0", 64-len(clientKey)) + clientKey
} else {
clientKey = nostr.GeneratePrivateKey()
}
bunker, err := nip46.ConnectBunker(c.Context, clientKey, bunkerURL, nil, func(s string) {
fmt.Fprintf(color.Error, color.CyanString("[nip46]: open the following URL: %s"), s)
})
return "", bunker, err
}
sec := c.String("sec") sec := c.String("sec")
if c.Bool("prompt-sec") { if c.Bool("prompt-sec") {
if isPiped() { if isPiped() {
return "", fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec") return "", nil, fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec")
} }
var err error sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil)
sec, err = speakeasy.FAsk(os.Stderr, "type your secret key as nsec or hex: ")
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get secret key: %w", err) return "", nil, fmt.Errorf("failed to get secret key: %w", err)
} }
} }
if strings.HasPrefix(sec, "nsec1") { if strings.HasPrefix(sec, "ncryptsec1") {
_, hex, err := nip19.Decode(sec) sec, err = promptDecrypt(sec)
if err != nil { if err != nil {
return "", fmt.Errorf("invalid nsec: %w", err) return "", nil, fmt.Errorf("failed to decrypt: %w", err)
} }
sec = hex.(string) } else if bsec, err := hex.DecodeString(strings.Repeat("0", 64-len(sec)) + sec); err == nil {
} sec = hex.EncodeToString(bsec)
if len(sec) > 64 { } else if prefix, hexvalue, err := nip19.Decode(sec); err != nil {
return "", fmt.Errorf("invalid secret key: too large") return "", nil, fmt.Errorf("invalid nsec: %w", err)
} } else if prefix == "nsec" {
sec = strings.Repeat("0", 64-len(sec)) + sec // left-pad sec = hexvalue.(string)
if err := validate32BytesHex(sec); err != nil {
return "", fmt.Errorf("invalid secret key")
} }
if ok := nostr.IsValid32ByteHex(sec); !ok {
return "", nil, fmt.Errorf("invalid secret key")
}
return sec, nil, nil
}
func promptDecrypt(ncryptsec1 string) (string, error) {
for i := 1; i < 4; i++ {
var attemptStr string
if i > 1 {
attemptStr = fmt.Sprintf(" [%d/3]", i)
}
password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil)
if err != nil {
return "", err
}
sec, err := nip49.Decrypt(ncryptsec1, password)
if err != nil {
continue
}
return sec, nil return sec, nil
} }
return "", fmt.Errorf("couldn't decrypt private key")
}
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {
config := &readline.Config{
Stdout: color.Error,
Prompt: color.YellowString(msg),
InterruptPrompt: "^C",
DisableAutoSaveHistory: true,
EnableMask: true,
MaskRune: '*',
}
return _ask(config, msg, "", shouldAskAgain)
}
func _ask(config *readline.Config, msg string, defaultValue string, shouldAskAgain func(answer string) bool) (string, error) {
rl, err := readline.NewEx(config)
if err != nil {
return "", err
}
rl.WriteStdin([]byte(defaultValue))
for {
answer, err := rl.Readline()
if err != nil {
return "", err
}
answer = strings.TrimSpace(strings.ToLower(answer))
if shouldAskAgain != nil && shouldAskAgain(answer) {
continue
}
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)
}

263
key.go Normal file
View File

@@ -0,0 +1,263 @@
package main
import (
"encoding/hex"
"encoding/json"
"fmt"
"os"
"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/nip19"
"github.com/nbd-wtf/go-nostr/nip49"
"github.com/urfave/cli/v2"
)
var key = &cli.Command{
Name: "key",
Usage: "operations on secret keys: generate, derive, encrypt, decrypt.",
Description: ``,
Subcommands: []*cli.Command{
generate,
public,
encrypt,
decrypt,
combine,
},
}
var generate = &cli.Command{
Name: "generate",
Usage: "generates a secret key",
Description: ``,
Action: func(c *cli.Context) error {
sec := nostr.GeneratePrivateKey()
stdout(sec)
return nil
},
}
var public = &cli.Command{
Name: "public",
Usage: "computes a public key from a secret key",
Description: ``,
ArgsUsage: "[secret]",
Action: func(c *cli.Context) error {
for sec := range getSecretKeysFromStdinLinesOrSlice(c, c.Args().Slice()) {
pubkey, err := nostr.GetPublicKey(sec)
if err != nil {
lineProcessingError(c, "failed to derive public key: %s", err)
continue
}
stdout(pubkey)
}
return nil
},
}
var encrypt = &cli.Command{
Name: "encrypt",
Usage: "encrypts a secret key and prints an ncryptsec code",
Description: `uses the NIP-49 standard.`,
ArgsUsage: "<secret> <password>",
Flags: []cli.Flag{
&cli.IntFlag{
Name: "logn",
Usage: "the bigger the number the harder it will be to bruteforce the password",
Value: 16,
DefaultText: "16",
},
},
Action: func(c *cli.Context) error {
var content string
var password string
switch c.Args().Len() {
case 1:
content = ""
password = c.Args().Get(0)
case 2:
content = c.Args().Get(0)
password = c.Args().Get(1)
}
if password == "" {
return fmt.Errorf("no password given")
}
for sec := range getSecretKeysFromStdinLinesOrSlice(c, []string{content}) {
ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02)
if err != nil {
lineProcessingError(c, "failed to encrypt: %s", err)
continue
}
stdout(ncryptsec)
}
return nil
},
}
var decrypt = &cli.Command{
Name: "decrypt",
Usage: "takes an ncrypsec and a password and decrypts it into an nsec",
Description: `uses the NIP-49 standard.`,
ArgsUsage: "<ncryptsec-code> <password>",
Action: func(c *cli.Context) error {
var content string
var password string
switch c.Args().Len() {
case 1:
content = ""
password = c.Args().Get(0)
case 2:
content = c.Args().Get(0)
password = c.Args().Get(1)
}
if password == "" {
return fmt.Errorf("no password given")
}
for ncryptsec := range getStdinLinesOrArgumentsFromSlice([]string{content}) {
sec, err := nip49.Decrypt(ncryptsec, password)
if err != nil {
lineProcessingError(c, "failed to decrypt: %s", err)
continue
}
nsec, _ := nip19.EncodePrivateKey(sec)
stdout(nsec)
}
return nil
},
}
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(c *cli.Context) 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(c *cli.Context, keys []string) chan string {
ch := make(chan string)
go func() {
for sec := range getStdinLinesOrArgumentsFromSlice(keys) {
if sec == "" {
continue
}
if strings.HasPrefix(sec, "nsec1") {
_, data, err := nip19.Decode(sec)
if err != nil {
lineProcessingError(c, "invalid nsec code: %s", err)
continue
}
sec = data.(string)
}
if !nostr.IsValid32ByteHex(sec) {
lineProcessingError(c, "invalid hex key")
continue
}
ch <- sec
}
close(ch)
}()
return ch
}

22
main.go
View File

@@ -1,14 +1,17 @@
package main package main
import ( import (
"fmt"
"os" "os"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
var q int
var app = &cli.App{ var app = &cli.App{
Name: "nak", Name: "nak",
Suggest: true,
UseShortOptionHandling: 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,
@@ -17,18 +20,23 @@ var app = &cli.App{
event, event,
decode, decode,
encode, encode,
key,
verify, verify,
relay, relay,
nsecbunker, bunker,
}, },
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.BoolFlag{ &cli.BoolFlag{
Name: "silent", Name: "quiet",
Usage: "do not print logs and info messages to stderr", Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout",
Aliases: []string{"s"}, Count: &q,
Aliases: []string{"q"},
Action: func(ctx *cli.Context, b bool) error { Action: func(ctx *cli.Context, b bool) error {
if b { if q >= 1 {
log = func(msg string, args ...any) {} log = func(msg string, args ...any) {}
if q >= 2 {
stdout = func(a ...any) (int, error) { return 0, nil }
}
} }
return nil return nil
}, },
@@ -38,7 +46,7 @@ var app = &cli.App{
func main() { func main() {
if err := app.Run(os.Args); err != nil { if err := app.Run(os.Args); err != nil {
fmt.Println(err) stdout(err)
os.Exit(1) os.Exit(1)
} }
} }

331
musig2.go Normal file
View 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
}

View File

@@ -1,123 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"net/url"
"os"
"github.com/bgentry/speakeasy"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip46"
"github.com/urfave/cli/v2"
)
var nsecbunker = &cli.Command{
Name: "nsecbunker",
Usage: "starts a NIP-46 signer daemon with the given --sec key",
ArgsUsage: "[relay...]",
Description: ``,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "sec",
Usage: "secret key to sign the event, as hex or nsec",
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.BoolFlag{
Name: "yes",
Aliases: []string{"y"},
Usage: "always respond to any NIP-46 requests from anyone",
},
},
Action: func(c *cli.Context) error {
// try to connect to the relays here
qs := url.Values{}
relayURLs := make([]string, 0, c.Args().Len())
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
_, relays := connectToAllRelays(c.Context, relayUrls)
if len(relays) == 0 {
log("failed to connect to any of the given relays.\n")
os.Exit(3)
}
for _, relay := range relays {
relayURLs = append(relayURLs, relay.URL)
qs.Add("relay", relay.URL)
}
}
if len(relayURLs) == 0 {
return fmt.Errorf("not connected to any relays: please specify at least one")
}
// gather the secret key
sec, err := gatherSecretKeyFromArguments(c)
if err != nil {
return err
}
pubkey, err := nostr.GetPublicKey(sec)
if err != nil {
return err
}
npub, _ := nip19.EncodePublicKey(pubkey)
log("listening at %s%v%s:\n %spubkey:%s %s\n %snpub:%s %s\n %sconnection code:%s %s\n %sbunker:%s %s\n\n",
BOLD_ON, relayURLs, BOLD_OFF,
BOLD_ON, BOLD_OFF, pubkey,
BOLD_ON, BOLD_OFF, npub,
BOLD_ON, BOLD_OFF, fmt.Sprintf("%s#secret?%s", npub, qs.Encode()),
BOLD_ON, BOLD_OFF, fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode()),
)
alwaysYes := c.Bool("yes")
// subscribe to relays
pool := nostr.NewSimplePool(c.Context)
events := pool.SubMany(c.Context, relayURLs, nostr.Filters{
{
Kinds: []int{24133},
Tags: nostr.TagMap{"p": []string{pubkey}},
},
})
signer := nip46.NewSigner(sec)
for ie := range events {
req, resp, eventResponse, harmless, err := signer.HandleRequest(ie.Event)
if err != nil {
log("< failed to handle request from %s: %w", ie.Event.PubKey, err)
continue
}
jreq, _ := json.MarshalIndent(req, " ", " ")
log("- got request from '%s': %s\n", ie.Event.PubKey, string(jreq))
jresp, _ := json.MarshalIndent(resp, " ", " ")
log("~ responding with %s\n", string(jresp))
if alwaysYes || harmless || askUserIfWeCanRespond() {
if err := ie.Relay.Publish(c.Context, eventResponse); err == nil {
log("* sent response!\n")
} else {
log("* failed to send response: %s\n", err)
}
}
}
return nil
},
}
func askUserIfWeCanRespond() bool {
answer, err := speakeasy.FAsk(os.Stderr,
fmt.Sprintf("proceed? y/n"))
if err != nil {
return false
}
if answer == "y" || answer == "yes" {
return true
}
return askUserIfWeCanRespond()
}

View File

@@ -16,7 +16,7 @@ var relay = &cli.Command{
nak relay nostr.wine`, nak relay nostr.wine`,
ArgsUsage: "<relay-url>", ArgsUsage: "<relay-url>",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) 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>")
} }
@@ -27,11 +27,13 @@ var relay = &cli.Command{
info, err := nip11.Fetch(c.Context, 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) lineProcessingError(c, "failed to fetch '%s' information document: %w", url, err)
continue
} }
pretty, _ := json.MarshalIndent(info, "", " ") pretty, _ := json.MarshalIndent(info, "", " ")
fmt.Println(string(pretty)) stdout(string(pretty))
}
return nil return nil
}, },
} }

74
req.go
View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"strconv"
"strings" "strings"
"github.com/mailru/easyjson" "github.com/mailru/easyjson"
@@ -61,13 +62,18 @@ example:
Usage: "shortcut for --tag p=<value>", Usage: "shortcut for --tag p=<value>",
Category: CATEGORY_FILTER_ATTRIBUTES, Category: CATEGORY_FILTER_ATTRIBUTES,
}, },
&cli.IntFlag{ &cli.StringSliceFlag{
Name: "d",
Usage: "shortcut for --tag d=<value>",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.StringFlag{
Name: "since", Name: "since",
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "only accept events newer than this (unix timestamp)", Usage: "only accept events newer than this (unix timestamp)",
Category: CATEGORY_FILTER_ATTRIBUTES, Category: CATEGORY_FILTER_ATTRIBUTES,
}, },
&cli.IntFlag{ &cli.StringFlag{
Name: "until", Name: "until",
Aliases: []string{"u"}, Aliases: []string{"u"},
Usage: "only accept events older than this (unix timestamp)", Usage: "only accept events older than this (unix timestamp)",
@@ -107,6 +113,15 @@ example:
Name: "prompt-sec", Name: "prompt-sec",
Usage: "prompt the user to paste a hex or nsec with which to sign the AUTH challenge", Usage: "prompt the user to paste a hex or nsec with which to sign the AUTH challenge",
}, },
&cli.StringFlag{
Name: "connect",
Usage: "sign AUTH 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",
},
}, },
ArgsUsage: "[relay...]", ArgsUsage: "[relay...]",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
@@ -118,13 +133,27 @@ example:
if !c.Bool("auth") { if !c.Bool("auth") {
return fmt.Errorf("auth not authorized") return fmt.Errorf("auth not authorized")
} }
sec, err := gatherSecretKeyFromArguments(c) sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(c)
if err != nil { if err != nil {
return err return err
} }
pk, _ := nostr.GetPublicKey(sec)
var pk string
if bunker != nil {
pk, err = bunker.GetPublicKey(c.Context)
if err != nil {
return fmt.Errorf("failed to get public key from bunker: %w", err)
}
} else {
pk, _ = nostr.GetPublicKey(sec)
}
log("performing auth as %s...\n", pk) log("performing auth as %s...\n", pk)
if bunker != nil {
return bunker.SignEvent(c.Context, evt)
} else {
return evt.Sign(sec) return evt.Sign(sec)
}
})) }))
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")
@@ -134,6 +163,12 @@ example:
for i, relay := range relays { for i, relay := range relays {
relayUrls[i] = relay.URL relayUrls[i] = relay.URL
} }
defer func() {
for _, relay := range relays {
relay.Close()
}
}()
} }
for stdinFilter := range getStdinLinesOrBlank() { for stdinFilter := range getStdinLinesOrBlank() {
@@ -172,6 +207,9 @@ example:
for _, ptag := range c.StringSlice("p") { for _, ptag := range c.StringSlice("p") {
tags = append(tags, []string{"p", ptag}) tags = append(tags, []string{"p", ptag})
} }
for _, dtag := range c.StringSlice("d") {
tags = append(tags, []string{"d", dtag})
}
if len(tags) > 0 && filter.Tags == nil { if len(tags) > 0 && filter.Tags == nil {
filter.Tags = make(nostr.TagMap) filter.Tags = make(nostr.TagMap)
@@ -184,16 +222,32 @@ example:
filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1]) filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1])
} }
if since := c.Int("since"); since != 0 { if since := c.String("since"); since != "" {
ts := nostr.Timestamp(since) if since == "now" {
ts := nostr.Now()
filter.Since = &ts filter.Since = &ts
} else if i, err := strconv.Atoi(since); err == nil {
ts := nostr.Timestamp(i)
filter.Since = &ts
} else {
return fmt.Errorf("parse error: Invalid numeric literal %q", since)
} }
if until := c.Int("until"); until != 0 { }
ts := nostr.Timestamp(until) if until := c.String("until"); until != "" {
if until == "now" {
ts := nostr.Now()
filter.Until = &ts filter.Until = &ts
} else if i, err := strconv.Atoi(until); err == nil {
ts := nostr.Timestamp(i)
filter.Until = &ts
} else {
return fmt.Errorf("parse error: Invalid numeric literal %q", until)
}
} }
if limit := c.Int("limit"); limit != 0 { if limit := c.Int("limit"); limit != 0 {
filter.Limit = limit filter.Limit = limit
} else if c.IsSet("limit") || c.Bool("stream") {
filter.LimitZero = true
} }
if len(relayUrls) > 0 { if len(relayUrls) > 0 {
@@ -202,7 +256,7 @@ example:
fn = pool.SubMany fn = pool.SubMany
} }
for ie := range fn(c.Context, relayUrls, nostr.Filters{filter}) { for ie := range fn(c.Context, relayUrls, nostr.Filters{filter}) {
fmt.Println(ie.Event) stdout(ie.Event)
} }
} else { } else {
// no relays given, will just print the filter // no relays given, will just print the filter
@@ -214,7 +268,7 @@ example:
result = string(j) result = string(j)
} }
fmt.Println(result) stdout(result)
} }
} }

View File

@@ -15,7 +15,7 @@ var verify = &cli.Command{
it outputs nothing if the verification is successful.`, it outputs nothing if the verification is successful.`,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
for stdinEvent := range getStdinLinesOrBlank() { 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 {