mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-08 16:48:51 +00:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71dfe583ed | ||
|
|
84bde7dacd | ||
|
|
81968f6c0c | ||
|
|
f198a46c19 | ||
|
|
c3ea9c15f6 | ||
|
|
8ddb9ce021 | ||
|
|
569d38a137 | ||
|
|
34c189af28 | ||
|
|
ffe2db7f96 | ||
|
|
c5f7926471 | ||
|
|
e008e08105 | ||
|
|
5dd5a7c699 | ||
|
|
347a82eaa9 | ||
|
|
e89823b10e | ||
|
|
6626001dd2 | ||
|
|
b7a7e0504f | ||
|
|
01e1f52a70 | ||
|
|
0b9e861f90 | ||
|
|
bda18e035a | ||
|
|
0d46d48881 | ||
|
|
6f24112b5e | ||
|
|
f4921f1fe9 | ||
|
|
3dfcec69b7 | ||
|
|
14b69f36cf | ||
|
|
3f7089e27e | ||
|
|
6a75c8aec3 | ||
|
|
b17887fe21 | ||
|
|
77103cae0c | ||
|
|
59a2c16b42 | ||
|
|
48d19196bb | ||
|
|
ad7010e506 | ||
|
|
584881266e | ||
|
|
a30f422d7d | ||
|
|
637b9440ef | ||
|
|
16c1e795bd | ||
|
|
8373da647e | ||
|
|
f295f130f2 | ||
|
|
5415fd369c | ||
|
|
f35cb4bd1d | ||
|
|
242b028656 | ||
|
|
f0d90b567c | ||
|
|
2d1e27f766 | ||
|
|
bfa72640cd | ||
|
|
e5b0b15908 | ||
|
|
0860cfcf6d | ||
|
|
b7b61c0723 | ||
|
|
ed3156ae10 | ||
|
|
30dbe2c1c0 | ||
|
|
4d75605c20 | ||
|
|
26b1aa359a | ||
|
|
bc7cd0939c | ||
|
|
5657fdc6a7 | ||
|
|
d9d36e7619 | ||
|
|
f2f9dda33a | ||
|
|
53cb2c0490 | ||
|
|
4a3c7dc825 | ||
|
|
05f2275c9e | ||
|
|
082be94614 | ||
|
|
15217f2466 | ||
|
|
8fbfdc65c8 | ||
|
|
11fe6b5809 | ||
|
|
6a7a5eb26e | ||
|
|
795e98bc2e | ||
|
|
4fdd80670a | ||
|
|
e507d90766 | ||
|
|
d95b6f50ff | ||
|
|
200e4e61f7 | ||
|
|
714d65312c | ||
|
|
5722061bf3 | ||
|
|
6f72d3c133 | ||
|
|
78932833df | ||
|
|
31b42c3499 |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,9 +1 @@
|
||||
target
|
||||
.bsp
|
||||
globals.bundle.js
|
||||
yarn.lock
|
||||
node_modules
|
||||
project/project
|
||||
.metals
|
||||
.bloop
|
||||
project/metals.sbt
|
||||
nak
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
version = 3.5.8
|
||||
runner.dialect = scala3
|
||||
24
LICENSE
Normal file
24
LICENSE
Normal file
@@ -0,0 +1,24 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <https://unlicense.org>
|
||||
19
README.md
19
README.md
@@ -16,7 +16,7 @@ take a look at the help text that comes in it to learn all possibilities, but he
|
||||
|
||||
### make a nostr event with custom content and tags, sign it with a different key and publish it to two relays
|
||||
```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"}
|
||||
publishing to wss://nostr-pub.wellorder.net... 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
|
||||
```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. "
|
||||
"ITM slaves!\n#gm https://image.nostr.build/cbbcdf80bfc302a6678ecf9387c87d87deca3e0e288a12e262926c34feb3f6aa.jpg "
|
||||
"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
|
||||
```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
|
||||
~> nak decode nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7
|
||||
{
|
||||
"id": "5ae731bbc7711f78513da14927c48cc7143a91e6cad0565fdc4d73b8967a7d59",
|
||||
"relays": [
|
||||
"wss://nostr.zbd.gg"
|
||||
"nostr.zbd.gg"
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -61,7 +61,7 @@ nak fetch nevent1qqs2e3k48vtrkzjm8vvyzcmsmkf58unrxtq2k4h5yspay6vhcqm4wqcpz9mhxue
|
||||
|
||||
### republish an event from one relay to multiple others
|
||||
```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"}
|
||||
publishing to wss://nostr.wine... failed: msg: blocked: not an active paid member
|
||||
publishing to wss://offchain.pub... success.
|
||||
@@ -76,3 +76,12 @@ publishing to wss://relayable.org... success.
|
||||
~> echo '{"content":"hello world","created_at":1698923350,"id":"05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a","kind":1,"pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","sig":"0a04a296321ed933858577f36fb2fb9a0933e966f9ee32b539493f5a4d00120891b1ca9152ebfbc04fb403bdaa7c73f415e7c4954e55726b4b4fa8cebf008cd6","tags":[]}' | nak verify
|
||||
invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a, got 05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a
|
||||
```
|
||||
|
||||
### fetch all quoted events by a given pubkey in their last 100 notes
|
||||
```shell
|
||||
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`.
|
||||
|
||||
191
bunker.go
Normal file
191
bunker.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"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.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 := gatherSecretKeyOrBunkerFromArguments(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pubkey, err := nostr.GetPublicKey(sec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
npub, _ := nip19.EncodePublicKey(pubkey)
|
||||
bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode())
|
||||
bold := color.New(color.Bold).Sprint
|
||||
|
||||
printBunkerInfo := func() {
|
||||
log("listening at %v:\n pubkey: %s \n npub: %s\n bunker: %s\n\n",
|
||||
bold(relayURLs),
|
||||
bold(pubkey),
|
||||
bold(npub),
|
||||
bold(bunkerURI),
|
||||
)
|
||||
}
|
||||
printBunkerInfo()
|
||||
|
||||
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.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) bool {
|
||||
return alwaysYes || harmless || askProceed(from)
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
}
|
||||
|
||||
var allowedSources = make([]string, 0, 2)
|
||||
|
||||
func askProceed(source string) bool {
|
||||
if slices.Contains(allowedSources, source) {
|
||||
return true
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "request from %s:\n", color.New(color.Bold, color.FgBlue).Sprint(source))
|
||||
res, err := ask(" proceed to fulfill this request? (yes/no/always from this) (y/n/a): ", "",
|
||||
func(answer string) bool {
|
||||
if answer != "y" && answer != "n" && answer != "a" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
switch res {
|
||||
case "n":
|
||||
return false
|
||||
case "y":
|
||||
return true
|
||||
case "a":
|
||||
allowedSources = append(allowedSources, source)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
4
count.go
4
count.go
@@ -78,7 +78,7 @@ var count = &cli.Command{
|
||||
|
||||
tags := make([][]string, 0, 5)
|
||||
for _, tagFlag := range c.StringSlice("tag") {
|
||||
spl := strings.Split(tagFlag, "=")
|
||||
spl := strings.SplitN(tagFlag, "=", 2)
|
||||
if len(spl) == 2 && len(spl[0]) == 1 {
|
||||
tags = append(tags, spl)
|
||||
} else {
|
||||
@@ -139,7 +139,7 @@ var count = &cli.Command{
|
||||
var result string
|
||||
j, _ := json.Marshal([]any{"COUNT", "nak", filter})
|
||||
result = string(j)
|
||||
fmt.Println(result)
|
||||
stdout(result)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
21
decode.go
21
decode.go
@@ -3,12 +3,11 @@ package main
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/nbd-wtf/go-nostr/sdk"
|
||||
sdk "github.com/nbd-wtf/nostr-sdk"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
@@ -34,11 +33,7 @@ var decode = &cli.Command{
|
||||
},
|
||||
ArgsUsage: "<npub | nprofile | nip05 | nevent | naddr | nsec>",
|
||||
Action: func(c *cli.Context) error {
|
||||
args := c.Args()
|
||||
if args.Len() != 1 {
|
||||
return fmt.Errorf("invalid number of arguments, need just one")
|
||||
}
|
||||
input := args.First()
|
||||
for input := range getStdinLinesOrArguments(c.Args()) {
|
||||
if strings.HasPrefix(input, "nostr:") {
|
||||
input = input[6:]
|
||||
}
|
||||
@@ -54,7 +49,8 @@ var decode = &cli.Command{
|
||||
decodeResult.HexResult.PrivateKey = hex.EncodeToString(b)
|
||||
decodeResult.HexResult.PublicKey = hex.EncodeToString(b)
|
||||
} else {
|
||||
return fmt.Errorf("hex string with invalid number of bytes: %d", len(b))
|
||||
lineProcessingError(c, "hex string with invalid number of bytes: %d", len(b))
|
||||
continue
|
||||
}
|
||||
} else if evp := sdk.InputToEventPointer(input); evp != nil {
|
||||
decodeResult = DecodeResult{EventPointer: evp}
|
||||
@@ -67,10 +63,15 @@ var decode = &cli.Command{
|
||||
decodeResult.PrivateKey.PrivateKey = value.(string)
|
||||
decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string))
|
||||
} else {
|
||||
return fmt.Errorf("couldn't decode input")
|
||||
lineProcessingError(c, "couldn't decode input '%s': %s", input, err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Println(decodeResult.JSON())
|
||||
stdout(decodeResult.JSON())
|
||||
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(c)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
116
encode.go
116
encode.go
@@ -1,10 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -28,36 +27,44 @@ var encode = &cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "npub",
|
||||
Usage: "encode a hex private key into bech32 'npub' format",
|
||||
Usage: "encode a hex public key into bech32 'npub' format",
|
||||
Action: func(c *cli.Context) error {
|
||||
target := getStdinOrFirstArgument(c)
|
||||
if err := validate32BytesHex(target); err != nil {
|
||||
return err
|
||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||
if ok := nostr.IsValidPublicKey(target); !ok {
|
||||
lineProcessingError(c, "invalid public key: %s", target)
|
||||
continue
|
||||
}
|
||||
|
||||
if npub, err := nip19.EncodePublicKey(target); err == nil {
|
||||
fmt.Println(npub)
|
||||
return nil
|
||||
stdout(npub)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(c)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "nsec",
|
||||
Usage: "encode a hex private key into bech32 'nsec' format",
|
||||
Action: func(c *cli.Context) error {
|
||||
target := getStdinOrFirstArgument(c)
|
||||
if err := validate32BytesHex(target); err != nil {
|
||||
return err
|
||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||
if ok := nostr.IsValid32ByteHex(target); !ok {
|
||||
lineProcessingError(c, "invalid private key: %s", target)
|
||||
continue
|
||||
}
|
||||
|
||||
if npub, err := nip19.EncodePrivateKey(target); err == nil {
|
||||
fmt.Println(npub)
|
||||
return nil
|
||||
stdout(npub)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(c)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -71,9 +78,10 @@ var encode = &cli.Command{
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
target := getStdinOrFirstArgument(c)
|
||||
if err := validate32BytesHex(target); err != nil {
|
||||
return err
|
||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||
if ok := nostr.IsValid32ByteHex(target); !ok {
|
||||
lineProcessingError(c, "invalid public key: %s", target)
|
||||
continue
|
||||
}
|
||||
|
||||
relays := c.StringSlice("relay")
|
||||
@@ -82,11 +90,14 @@ var encode = &cli.Command{
|
||||
}
|
||||
|
||||
if npub, err := nip19.EncodeProfile(target, relays); err == nil {
|
||||
fmt.Println(npub)
|
||||
return nil
|
||||
stdout(npub)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(c)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -104,15 +115,16 @@ var encode = &cli.Command{
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
target := getStdinOrFirstArgument(c)
|
||||
if err := validate32BytesHex(target); err != nil {
|
||||
return err
|
||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||
if ok := nostr.IsValid32ByteHex(target); !ok {
|
||||
lineProcessingError(c, "invalid event id: %s", target)
|
||||
continue
|
||||
}
|
||||
|
||||
author := c.String("author")
|
||||
if author != "" {
|
||||
if err := validate32BytesHex(author); err != nil {
|
||||
return err
|
||||
if ok := nostr.IsValidPublicKey(author); !ok {
|
||||
return fmt.Errorf("invalid 'author' public key")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,11 +134,14 @@ var encode = &cli.Command{
|
||||
}
|
||||
|
||||
if npub, err := nip19.EncodeEvent(target, relays, author); err == nil {
|
||||
fmt.Println(npub)
|
||||
return nil
|
||||
stdout(npub)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(c)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -136,7 +151,7 @@ var encode = &cli.Command{
|
||||
&cli.StringFlag{
|
||||
Name: "identifier",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "the \"d\" tag identifier of this replaceable event",
|
||||
Usage: "the \"d\" tag identifier of this replaceable event -- can also be read from stdin",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
@@ -158,9 +173,10 @@ var encode = &cli.Command{
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
for d := range getStdinLinesOrBlank() {
|
||||
pubkey := c.String("pubkey")
|
||||
if err := validate32BytesHex(pubkey); err != nil {
|
||||
return err
|
||||
if ok := nostr.IsValidPublicKey(pubkey); !ok {
|
||||
return fmt.Errorf("invalid 'pubkey'")
|
||||
}
|
||||
|
||||
kind := c.Int("kind")
|
||||
@@ -168,9 +184,12 @@ var encode = &cli.Command{
|
||||
return fmt.Errorf("kind must be between 30000 and 39999, as per NIP-16, got %d", kind)
|
||||
}
|
||||
|
||||
d := c.String("identifier")
|
||||
if d == "" {
|
||||
return fmt.Errorf("\"d\" tag identifier can't be empty")
|
||||
d = c.String("identifier")
|
||||
if d == "" {
|
||||
lineProcessingError(c, "\"d\" tag identifier can't be empty")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
relays := c.StringSlice("relay")
|
||||
@@ -179,43 +198,36 @@ var encode = &cli.Command{
|
||||
}
|
||||
|
||||
if npub, err := nip19.EncodeEntity(pubkey, kind, d, relays); err == nil {
|
||||
fmt.Println(npub)
|
||||
return nil
|
||||
stdout(npub)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(c)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "note",
|
||||
Usage: "generate note1 event codes (not recommended)",
|
||||
Action: func(c *cli.Context) error {
|
||||
target := getStdinOrFirstArgument(c)
|
||||
if err := validate32BytesHex(target); err != nil {
|
||||
return err
|
||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||
if ok := nostr.IsValid32ByteHex(target); !ok {
|
||||
lineProcessingError(c, "invalid event id: %s", target)
|
||||
continue
|
||||
}
|
||||
|
||||
if npub, err := nip19.EncodeNote(target); err == nil {
|
||||
fmt.Println(npub)
|
||||
return nil
|
||||
if note, err := nip19.EncodeNote(target); err == nil {
|
||||
stdout(note)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(c)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
215
event.go
215
event.go
@@ -11,8 +11,10 @@ import (
|
||||
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/nbd-wtf/go-nostr/nson"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const CATEGORY_EVENT_FIELDS = "EVENT FIELDS"
|
||||
@@ -30,19 +32,64 @@ if an event -- or a partial event -- is given on stdin, the flags can be used to
|
||||
|
||||
example:
|
||||
echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak event wss://offchain.pub
|
||||
echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'
|
||||
`,
|
||||
echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "sec",
|
||||
Usage: "secret key to sign the event",
|
||||
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.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{
|
||||
Name: "envelope",
|
||||
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "auth",
|
||||
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{
|
||||
Name: "nson",
|
||||
Usage: "encode the event using NSON",
|
||||
@@ -79,6 +126,11 @@ example:
|
||||
Usage: "shortcut for --tag p=<value>",
|
||||
Category: CATEGORY_EVENT_FIELDS,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "d",
|
||||
Usage: "shortcut for --tag d=<value>",
|
||||
Category: CATEGORY_EVENT_FIELDS,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "created-at",
|
||||
Aliases: []string{"time", "ts"},
|
||||
@@ -90,22 +142,50 @@ example:
|
||||
},
|
||||
ArgsUsage: "[relay...]",
|
||||
Action: func(c *cli.Context) error {
|
||||
// try to connect to the relays here
|
||||
var relays []*nostr.Relay
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
for _, relay := range relays {
|
||||
relay.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doAuth := c.Bool("auth")
|
||||
|
||||
// then process input and generate events
|
||||
for stdinEvent := range getStdinLinesOrBlank() {
|
||||
evt := nostr.Event{
|
||||
Tags: make(nostr.Tags, 0, 3),
|
||||
}
|
||||
|
||||
kindWasSupplied := false
|
||||
mustRehashAndResign := false
|
||||
|
||||
if stdinEvent := getStdin(); stdinEvent != "" {
|
||||
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
||||
return fmt.Errorf("invalid event received from stdin: %w", err)
|
||||
if stdinEvent != "" {
|
||||
if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
||||
lineProcessingError(c, "invalid event received from stdin: %s", err)
|
||||
continue
|
||||
}
|
||||
kindWasSupplied = strings.Contains(stdinEvent, `"kind"`)
|
||||
}
|
||||
|
||||
if kind := c.Int("kind"); kind != 0 {
|
||||
if kind := c.Int("kind"); slices.Contains(c.FlagNames(), "kind") {
|
||||
evt.Kind = kind
|
||||
mustRehashAndResign = true
|
||||
} else if evt.Kind == 0 {
|
||||
} else if !kindWasSupplied {
|
||||
evt.Kind = 1
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
@@ -121,22 +201,26 @@ example:
|
||||
tags := make(nostr.Tags, 0, 5)
|
||||
for _, tagFlag := range c.StringSlice("tag") {
|
||||
// tags are in the format key=value
|
||||
spl := strings.Split(tagFlag, "=")
|
||||
if len(spl) == 2 && len(spl[0]) > 0 {
|
||||
tag := nostr.Tag{spl[0]}
|
||||
tagName, tagValue, found := strings.Cut(tagFlag, "=")
|
||||
tag := []string{tagName}
|
||||
if found {
|
||||
// tags may also contain extra elements separated with a ";"
|
||||
spl2 := strings.Split(spl[1], ";")
|
||||
tag = append(tag, spl2...)
|
||||
// ~
|
||||
tags = append(tags, tag)
|
||||
tagValues := strings.Split(tagValue, ";")
|
||||
tag = append(tag, tagValues...)
|
||||
}
|
||||
tags = tags.AppendUnique(tag)
|
||||
}
|
||||
|
||||
for _, etag := range c.StringSlice("e") {
|
||||
tags = append(tags, []string{"e", etag})
|
||||
tags = tags.AppendUnique([]string{"e", etag})
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
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
|
||||
}
|
||||
if len(tags) > 0 {
|
||||
@@ -163,32 +247,34 @@ example:
|
||||
}
|
||||
|
||||
if evt.Sig == "" || mustRehashAndResign {
|
||||
if err := evt.Sign(c.String("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)
|
||||
}
|
||||
}
|
||||
|
||||
relays := c.Args().Slice()
|
||||
if len(relays) > 0 {
|
||||
fmt.Println(evt.String())
|
||||
for _, url := range relays {
|
||||
fmt.Fprintf(os.Stderr, "publishing to %s... ", url)
|
||||
if relay, err := nostr.RelayConnect(c.Context, url); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err)
|
||||
} else {
|
||||
ctx, cancel := context.WithTimeout(c.Context, 10*time.Second)
|
||||
defer cancel()
|
||||
if status, err := relay.Publish(ctx, evt); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed: %s\n", err)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "%s.\n", status)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// print event as json
|
||||
var result string
|
||||
if c.Bool("envelope") {
|
||||
j, _ := json.Marshal([]any{"EVENT", evt})
|
||||
j, _ := json.Marshal(nostr.EventEnvelope{Event: evt})
|
||||
result = string(j)
|
||||
} else if c.Bool("nson") {
|
||||
result, _ = nson.Marshal(&evt)
|
||||
@@ -196,9 +282,62 @@ example:
|
||||
j, _ := easyjson.Marshal(&evt)
|
||||
result = string(j)
|
||||
}
|
||||
fmt.Println(result)
|
||||
stdout(result)
|
||||
|
||||
// publish to relays
|
||||
successRelays := make([]string, 0, len(relays))
|
||||
if len(relays) > 0 {
|
||||
os.Stdout.Sync()
|
||||
for _, relay := range relays {
|
||||
publish:
|
||||
log("publishing to %s... ", relay.URL)
|
||||
ctx, cancel := context.WithTimeout(c.Context, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := relay.Publish(ctx, evt)
|
||||
if err == nil {
|
||||
// published fine
|
||||
log("success.\n")
|
||||
successRelays = append(successRelays, relay.URL)
|
||||
continue // continue to next relay
|
||||
}
|
||||
|
||||
// error publishing
|
||||
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
|
||||
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)
|
||||
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
|
||||
doAuth = false
|
||||
goto publish
|
||||
} else {
|
||||
log("auth error: %s. ", err)
|
||||
}
|
||||
}
|
||||
log("failed: %s\n", err)
|
||||
}
|
||||
if len(successRelays) > 0 && c.Bool("nevent") {
|
||||
nevent, _ := nip19.EncodeEvent(evt.ID, successRelays, evt.PubKey)
|
||||
log(nevent + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(c)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
135
example_test.go
Normal file
135
example_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package main
|
||||
|
||||
import "os"
|
||||
|
||||
func ExampleEventBasic() {
|
||||
app.Run([]string{"nak", "event", "--ts", "1699485669"})
|
||||
// Output:
|
||||
// {"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"}
|
||||
}
|
||||
|
||||
// (for some reason there can only be one test dealing with stdin in the suite otherwise it halts)
|
||||
func ExampleEventParsingFromStdin() {
|
||||
prevStdin := os.Stdin
|
||||
defer func() { os.Stdin = prevStdin }()
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdin = r
|
||||
w.WriteString("{\"content\":\"hello world\"}\n{\"content\":\"hello sun\"}\n")
|
||||
app.Run([]string{"nak", "event", "-t", "t=spam", "--ts", "1699485669"})
|
||||
// Output:
|
||||
// {"id":"bda134f9077c11973afe6aa5a1cc6f5bcea01c40d318b8f91dcb8e50507cfa52","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello world","sig":"7552454bb8e7944230142634e3e34ac7468bad9b21ed6909da572c611018dff1d14d0792e98b5806f6330edc51e09efa6d0b66a9694dc34606c70f4e580e7493"}
|
||||
// {"id":"879c36ec73acca288825b53585389581d3836e7f0fe4d46e5eba237ca56d6af5","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello sun","sig":"6c7e6b13ebdf931d26acfdd00bec2ec1140ddaf8d1ed61453543a14e729a460fe36c40c488ccb194a0e1ab9511cb6c36741485f501bdb93c39ca4c51bc59cbd4"}
|
||||
}
|
||||
|
||||
func ExampleEventComplex() {
|
||||
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() {
|
||||
app.Run([]string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"})
|
||||
// Output:
|
||||
// ["REQ","nak",{"kinds":[1],"authors":["2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"],"limit":18,"#e":["aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"]}]
|
||||
}
|
||||
|
||||
func ExampleReqIdFromRelay() {
|
||||
app.Run([]string{"nak", "req", "-i", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "wss://nostr.wine"})
|
||||
// Output:
|
||||
// {"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 ExampleMultipleFetch() {
|
||||
app.Run([]string{"nak", "fetch", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"})
|
||||
// Output:
|
||||
// {"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"
|
||||
// }
|
||||
}
|
||||
35
fetch.go
35
fetch.go
@@ -1,11 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/nbd-wtf/go-nostr/sdk"
|
||||
sdk "github.com/nbd-wtf/nostr-sdk"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
@@ -24,12 +22,22 @@ var fetch = &cli.Command{
|
||||
},
|
||||
ArgsUsage: "[nip19code]",
|
||||
Action: func(c *cli.Context) error {
|
||||
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{}
|
||||
code := getStdinOrFirstArgument(c)
|
||||
|
||||
prefix, value, err := nip19.Decode(code)
|
||||
if err != nil {
|
||||
return err
|
||||
lineProcessingError(c, "failed to decode: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
relays := c.StringSlice("relay")
|
||||
@@ -45,20 +53,20 @@ var fetch = &cli.Command{
|
||||
if v.Author != "" {
|
||||
authorHint = v.Author
|
||||
}
|
||||
relays = v.Relays
|
||||
relays = append(relays, v.Relays...)
|
||||
case "naddr":
|
||||
v := value.(nostr.EntityPointer)
|
||||
filter.Tags = nostr.TagMap{"d": []string{v.Identifier}}
|
||||
filter.Kinds = append(filter.Kinds, v.Kind)
|
||||
filter.Authors = append(filter.Authors, v.PublicKey)
|
||||
authorHint = v.PublicKey
|
||||
relays = v.Relays
|
||||
relays = append(relays, v.Relays...)
|
||||
case "nprofile":
|
||||
v := value.(nostr.ProfilePointer)
|
||||
filter.Authors = append(filter.Authors, v.PublicKey)
|
||||
filter.Kinds = append(filter.Kinds, 0)
|
||||
authorHint = v.PublicKey
|
||||
relays = v.Relays
|
||||
relays = append(relays, v.Relays...)
|
||||
case "npub":
|
||||
v := value.(string)
|
||||
filter.Authors = append(filter.Authors, v)
|
||||
@@ -66,10 +74,10 @@ var fetch = &cli.Command{
|
||||
authorHint = v
|
||||
}
|
||||
|
||||
pool := nostr.NewSimplePool(c.Context)
|
||||
if 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 {
|
||||
if relayListItem.Outbox {
|
||||
relays = append(relays, relayListItem.URL)
|
||||
@@ -78,13 +86,16 @@ var fetch = &cli.Command{
|
||||
}
|
||||
|
||||
if len(relays) == 0 {
|
||||
return fmt.Errorf("no relay hints found")
|
||||
lineProcessingError(c, "no relay hints found")
|
||||
continue
|
||||
}
|
||||
|
||||
for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) {
|
||||
fmt.Println(ie.Event)
|
||||
stdout(ie.Event)
|
||||
}
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(c)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
43
go.mod
43
go.mod
@@ -1,35 +1,42 @@
|
||||
module github.com/fiatjaf/nak
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.0
|
||||
|
||||
require (
|
||||
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/nbd-wtf/go-nostr v0.24.2
|
||||
github.com/urfave/cli/v2 v2.25.3
|
||||
github.com/nbd-wtf/go-nostr v0.30.2
|
||||
github.com/nbd-wtf/nostr-sdk v0.0.5
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
|
||||
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // 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/decred/dcrd/crypto/blake256 v1.0.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||
github.com/fiatjaf/eventstore v0.2.16 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.2.0 // indirect
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||
github.com/gobwas/ws v1.3.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.0.2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/tidwall/gjson v1.14.4 // indirect
|
||||
github.com/tidwall/gjson v1.17.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
golang.org/x/exp v0.0.0-20221106115401-f9659909a136 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/crypto v0.7.0 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
)
|
||||
|
||||
91
go.sum
91
go.sum
@@ -4,15 +4,18 @@ github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tj
|
||||
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
|
||||
github.com/btcsuite/btcd/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.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0=
|
||||
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.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/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.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
|
||||
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.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
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/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||
@@ -22,35 +25,39 @@ 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/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/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
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/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 v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
|
||||
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/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.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||
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/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
|
||||
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||
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 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg=
|
||||
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.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
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/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I=
|
||||
github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
|
||||
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
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.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
@@ -71,8 +78,15 @@ 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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/nbd-wtf/go-nostr v0.24.2 h1:1PdFED7uHh3BlXfDVD96npBc0YAgj9hPT+l6NWog4kc=
|
||||
github.com/nbd-wtf/go-nostr v0.24.2/go.mod h1:eE8Qf8QszZbCd9arBQyotXqATNUElWsTEEx+LLORhyQ=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
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.30.2 h1:dG/2X52/XDg+7phZH+BClcvA5D+S6dXvxJKkBaySEzI=
|
||||
github.com/nbd-wtf/go-nostr v0.30.2/go.mod h1:tiKJY6fWYSujbTQb201Y+IQ3l4szqYVt+fsTnsm7FCk=
|
||||
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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -82,40 +96,42 @@ 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.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.0 h1:2k4qrO/orvmEXZ3hmtHqIy9XaQtPTwzMZk1+iErpE8c=
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.0/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
|
||||
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY=
|
||||
github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20221106115401-f9659909a136 h1:Fq7F/w7MAa1KJ5bt2aJ62ihqp9HDcRuyILskkpIAurw=
|
||||
golang.org/x/exp v0.0.0-20221106115401-f9659909a136/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -126,12 +142,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-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-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
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.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.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.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.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/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=
|
||||
@@ -145,8 +164,8 @@ 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/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.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.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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
228
helpers.go
228
helpers.go
@@ -1,34 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
"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/nbd-wtf/go-nostr/nip49"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func getStdin() string {
|
||||
stat, _ := os.Stdin.Stat()
|
||||
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
||||
read := bytes.NewBuffer(make([]byte, 0, 1000))
|
||||
_, err := io.Copy(read, os.Stdin)
|
||||
if err == nil {
|
||||
return strings.TrimSpace(read.String())
|
||||
}
|
||||
}
|
||||
return ""
|
||||
const (
|
||||
LINE_PROCESSING_ERROR = iota
|
||||
)
|
||||
|
||||
var log = func(msg string, args ...any) {
|
||||
fmt.Fprintf(color.Error, msg, args...)
|
||||
}
|
||||
|
||||
func getStdinOrFirstArgument(c *cli.Context) string {
|
||||
target := c.Args().First()
|
||||
if target != "" {
|
||||
return target
|
||||
var stdout = fmt.Println
|
||||
|
||||
func isPiped() bool {
|
||||
stat, _ := os.Stdin.Stat()
|
||||
return stat.Mode()&os.ModeCharDevice == 0
|
||||
}
|
||||
|
||||
func getStdinLinesOrBlank() chan string {
|
||||
multi := make(chan string)
|
||||
if hasStdinLines := writeStdinLinesOrNothing(multi); !hasStdinLines {
|
||||
single := make(chan string, 1)
|
||||
single <- ""
|
||||
close(single)
|
||||
return single
|
||||
} else {
|
||||
return multi
|
||||
}
|
||||
}
|
||||
|
||||
func getStdinLinesOrArguments(args cli.Args) chan string {
|
||||
return getStdinLinesOrArgumentsFromSlice(args.Slice())
|
||||
}
|
||||
|
||||
func getStdinLinesOrArgumentsFromSlice(args []string) chan string {
|
||||
// try the first argument
|
||||
if len(args) > 0 {
|
||||
argsCh := make(chan string, 1)
|
||||
go func() {
|
||||
for _, arg := range args {
|
||||
argsCh <- arg
|
||||
}
|
||||
close(argsCh)
|
||||
}()
|
||||
return argsCh
|
||||
}
|
||||
|
||||
// try the stdin
|
||||
multi := make(chan string)
|
||||
writeStdinLinesOrNothing(multi)
|
||||
return multi
|
||||
}
|
||||
|
||||
func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) {
|
||||
if isPiped() {
|
||||
// piped
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
scanner.Buffer(make([]byte, 16*1024), 256*1024)
|
||||
hasEmittedAtLeastOne := false
|
||||
for scanner.Scan() {
|
||||
ch <- strings.TrimSpace(scanner.Text())
|
||||
hasEmittedAtLeastOne = true
|
||||
}
|
||||
if !hasEmittedAtLeastOne {
|
||||
ch <- ""
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return true
|
||||
} else {
|
||||
// not piped
|
||||
return false
|
||||
}
|
||||
return getStdin()
|
||||
}
|
||||
|
||||
func validateRelayURLs(wsurls []string) error {
|
||||
@@ -49,3 +109,137 @@ func validateRelayURLs(wsurls []string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func connectToAllRelays(
|
||||
ctx context.Context,
|
||||
relayUrls []string,
|
||||
opts ...nostr.PoolOption,
|
||||
) (*nostr.SimplePool, []*nostr.Relay) {
|
||||
relays := make([]*nostr.Relay, 0, len(relayUrls))
|
||||
pool := nostr.NewSimplePool(ctx, opts...)
|
||||
for _, url := range relayUrls {
|
||||
log("connecting to %s... ", url)
|
||||
if relay, err := pool.EnsureRelay(url); err == nil {
|
||||
relays = append(relays, relay)
|
||||
log("ok.\n")
|
||||
} else {
|
||||
log(err.Error() + "\n")
|
||||
}
|
||||
}
|
||||
return pool, relays
|
||||
}
|
||||
|
||||
func lineProcessingError(c *cli.Context, msg string, args ...any) {
|
||||
c.Context = context.WithValue(c.Context, LINE_PROCESSING_ERROR, true)
|
||||
log(msg+"\n", args...)
|
||||
}
|
||||
|
||||
func exitIfLineProcessingError(c *cli.Context) {
|
||||
if val := c.Context.Value(LINE_PROCESSING_ERROR); val != nil && val.(bool) {
|
||||
os.Exit(123)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
if c.Bool("prompt-sec") {
|
||||
if isPiped() {
|
||||
return "", nil, fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec")
|
||||
}
|
||||
sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to get secret key: %w", err)
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(sec, "ncryptsec1") {
|
||||
sec, err = promptDecrypt(sec)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to decrypt: %w", err)
|
||||
}
|
||||
} else if bsec, err := hex.DecodeString(strings.Repeat("0", 64-len(sec)) + sec); err == nil {
|
||||
sec = hex.EncodeToString(bsec)
|
||||
} else if prefix, hexvalue, err := nip19.Decode(sec); err != nil {
|
||||
return "", nil, fmt.Errorf("invalid nsec: %w", err)
|
||||
} else if prefix == "nsec" {
|
||||
sec = hexvalue.(string)
|
||||
}
|
||||
|
||||
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 "", fmt.Errorf("couldn't decrypt private key")
|
||||
}
|
||||
|
||||
func ask(msg string, defaultValue string, shouldAskAgain func(answer string) bool) (string, error) {
|
||||
return _ask(&readline.Config{
|
||||
Stdout: color.Error,
|
||||
Prompt: color.YellowString(msg),
|
||||
InterruptPrompt: "^C",
|
||||
DisableAutoSaveHistory: true,
|
||||
}, msg, defaultValue, shouldAskAgain)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
184
key.go
Normal file
184
key.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"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.`,
|
||||
ArgsUsage: "[pubkey...]",
|
||||
Action: func(c *cli.Context) error {
|
||||
keys := make([]*btcec.PublicKey, 0, 5)
|
||||
for _, pub := range c.Args().Slice() {
|
||||
keyb, err := hex.DecodeString(pub)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing key %s: %w", pub, err)
|
||||
}
|
||||
|
||||
pubk, err := btcec.ParsePubKey(keyb)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing key %s: %w", pub, err)
|
||||
}
|
||||
|
||||
keys = append(keys, pubk)
|
||||
}
|
||||
|
||||
agg, _, _, err := musig2.AggregateKeys(keys, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(hex.EncodeToString(agg.FinalKey.SerializeCompressed()))
|
||||
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
|
||||
}
|
||||
31
main.go
31
main.go
@@ -1,15 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := &cli.App{
|
||||
var q int
|
||||
|
||||
var app = &cli.App{
|
||||
Name: "nak",
|
||||
Suggest: true,
|
||||
UseShortOptionHandling: true,
|
||||
Usage: "the nostr army knife command-line tool",
|
||||
Commands: []*cli.Command{
|
||||
req,
|
||||
@@ -18,12 +20,33 @@ func main() {
|
||||
event,
|
||||
decode,
|
||||
encode,
|
||||
key,
|
||||
verify,
|
||||
relay,
|
||||
bunker,
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout",
|
||||
Count: &q,
|
||||
Aliases: []string{"q"},
|
||||
Action: func(ctx *cli.Context, b bool) error {
|
||||
if q >= 1 {
|
||||
log = func(msg string, args ...any) {}
|
||||
if q >= 2 {
|
||||
stdout = func(a ...any) (int, error) { return 0, nil }
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fmt.Println(err)
|
||||
stdout(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
331
musig2.go
Normal file
331
musig2.go
Normal file
@@ -0,0 +1,331 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func performMusig(
|
||||
ctx context.Context,
|
||||
sec string,
|
||||
evt *nostr.Event,
|
||||
numSigners int,
|
||||
keys []string,
|
||||
nonces []string,
|
||||
secNonce string,
|
||||
partialSigs []string,
|
||||
) (signed bool, err error) {
|
||||
// preprocess data received
|
||||
secb, err := hex.DecodeString(sec)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
seck, pubk := btcec.PrivKeyFromBytes(secb)
|
||||
|
||||
knownSigners := make([]*btcec.PublicKey, 0, numSigners)
|
||||
includesUs := false
|
||||
for _, hexpub := range keys {
|
||||
bpub, err := hex.DecodeString(hexpub)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
spub, err := btcec.ParsePubKey(bpub)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
knownSigners = append(knownSigners, spub)
|
||||
|
||||
if spub.IsEqual(pubk) {
|
||||
includesUs = true
|
||||
}
|
||||
}
|
||||
if !includesUs {
|
||||
knownSigners = append(knownSigners, pubk)
|
||||
}
|
||||
|
||||
knownNonces := make([][66]byte, 0, numSigners)
|
||||
for _, hexnonce := range nonces {
|
||||
bnonce, err := hex.DecodeString(hexnonce)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(bnonce) != 66 {
|
||||
return false, fmt.Errorf("nonce is not 66 bytes: %s", hexnonce)
|
||||
}
|
||||
var b66nonce [66]byte
|
||||
copy(b66nonce[:], bnonce)
|
||||
knownNonces = append(knownNonces, b66nonce)
|
||||
}
|
||||
|
||||
knownPartialSigs := make([]*musig2.PartialSignature, 0, numSigners)
|
||||
for _, hexps := range partialSigs {
|
||||
bps, err := hex.DecodeString(hexps)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var ps musig2.PartialSignature
|
||||
if err := ps.Decode(bytes.NewBuffer(bps)); err != nil {
|
||||
return false, fmt.Errorf("invalid partial signature %s: %w", hexps, err)
|
||||
}
|
||||
knownPartialSigs = append(knownPartialSigs, &ps)
|
||||
}
|
||||
|
||||
// create the context
|
||||
var mctx *musig2.Context
|
||||
if len(knownSigners) < numSigners {
|
||||
// we don't know all the signers yet
|
||||
mctx, err = musig2.NewContext(seck, true,
|
||||
musig2.WithNumSigners(numSigners),
|
||||
musig2.WithEarlyNonceGen(),
|
||||
)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create signing context with %d unknown signers: %w",
|
||||
numSigners, err)
|
||||
}
|
||||
} else {
|
||||
// we know all the signers
|
||||
mctx, err = musig2.NewContext(seck, true,
|
||||
musig2.WithKnownSigners(knownSigners),
|
||||
)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create signing context with %d known signers: %w",
|
||||
len(knownSigners), err)
|
||||
}
|
||||
}
|
||||
|
||||
// nonce generation phase -- for sharing
|
||||
if len(knownSigners) < numSigners {
|
||||
// if we don't have all the signers we just generate a nonce and yield it to the next people
|
||||
nonce, err := mctx.EarlySessionNonce()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "the following code should be saved secretly until the next step an included with --musig-nonce-secret:\n")
|
||||
fmt.Fprintf(os.Stderr, "%s\n\n", base64.StdEncoding.EncodeToString(nonce.SecNonce[:]))
|
||||
|
||||
knownNonces = append(knownNonces, nonce.PubNonce)
|
||||
printPublicCommandForNextPeer(evt, numSigners, knownSigners, knownNonces, nil, false)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// if we got here we have all the pubkeys, so we can print the combined key
|
||||
if comb, err := mctx.CombinedKey(); err != nil {
|
||||
return false, fmt.Errorf("failed to combine keys (after %d signers): %w", len(knownSigners), err)
|
||||
} else {
|
||||
evt.PubKey = hex.EncodeToString(comb.SerializeCompressed()[1:])
|
||||
evt.ID = evt.GetID()
|
||||
fmt.Fprintf(os.Stderr, "combined key: %x\n\n", comb.SerializeCompressed())
|
||||
}
|
||||
|
||||
// we have all the signers, which means we must also have all the nonces
|
||||
var session *musig2.Session
|
||||
if len(keys) == numSigners-1 {
|
||||
// if we were the last to include our key, that means we have to include our nonce here to
|
||||
// i.e. we didn't input our own pub nonce in the parameters
|
||||
session, err = mctx.NewSession()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create session as the last peer to include our key: %w", err)
|
||||
}
|
||||
knownNonces = append(knownNonces, session.PublicNonce())
|
||||
} else {
|
||||
// otherwise we have included our own nonce in the parameters (from copypasting) but must
|
||||
// also include the secret nonce that wasn't shared with peers
|
||||
if secNonce == "" {
|
||||
return false, fmt.Errorf("missing --musig-nonce-secret value")
|
||||
}
|
||||
secNonceB, err := base64.StdEncoding.DecodeString(secNonce)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid --musig-nonce-secret: %w", err)
|
||||
}
|
||||
var secNonce97 [97]byte
|
||||
copy(secNonce97[:], secNonceB)
|
||||
session, err = mctx.NewSession(musig2.WithPreGeneratedNonce(&musig2.Nonces{
|
||||
SecNonce: secNonce97,
|
||||
PubNonce: secNonceToPubNonce(secNonce97),
|
||||
}))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create signing session with secret nonce: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var noncesOk bool
|
||||
for _, b66nonce := range knownNonces {
|
||||
if b66nonce == session.PublicNonce() {
|
||||
// don't add our own nonce
|
||||
continue
|
||||
}
|
||||
|
||||
noncesOk, err = session.RegisterPubNonce(b66nonce)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to register nonce: %w", err)
|
||||
}
|
||||
}
|
||||
if !noncesOk {
|
||||
return false, fmt.Errorf("we've registered all the nonces we had but at least one is missing, this shouldn't happen")
|
||||
}
|
||||
|
||||
// signing phase
|
||||
// we always have to sign, so let's do this
|
||||
id := evt.GetID()
|
||||
hash, _ := hex.DecodeString(id)
|
||||
var msg32 [32]byte
|
||||
copy(msg32[:], hash)
|
||||
partialSig, err := session.Sign(msg32) // this will already include our sig in the bundle
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to produce partial signature: %w", err)
|
||||
}
|
||||
|
||||
if len(knownPartialSigs)+1 < len(knownSigners) {
|
||||
// still missing some signatures
|
||||
knownPartialSigs = append(knownPartialSigs, partialSig) // we include ours here just so it's printed
|
||||
printPublicCommandForNextPeer(evt, numSigners, knownSigners, knownNonces, knownPartialSigs, true)
|
||||
return false, nil
|
||||
} else {
|
||||
// we have all signatures
|
||||
for _, ps := range knownPartialSigs {
|
||||
_, err = session.CombineSig(ps)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to combine partial signature: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we have the signature
|
||||
evt.Sig = hex.EncodeToString(session.FinalSig().Serialize())
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func printPublicCommandForNextPeer(
|
||||
evt *nostr.Event,
|
||||
numSigners int,
|
||||
knownSigners []*btcec.PublicKey,
|
||||
knownNonces [][66]byte,
|
||||
knownPartialSigs []*musig2.PartialSignature,
|
||||
includeNonceSecret bool,
|
||||
) {
|
||||
maybeNonceSecret := ""
|
||||
if includeNonceSecret {
|
||||
maybeNonceSecret = " --musig-nonce-secret '<insert-nonce-secret>'"
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "the next signer and they should call this on their side:\nnak event --sec <insert-secret-key> --musig %d %s%s%s%s%s\n",
|
||||
numSigners,
|
||||
eventToCliArgs(evt),
|
||||
signersToCliArgs(knownSigners),
|
||||
noncesToCliArgs(knownNonces),
|
||||
partialSigsToCliArgs(knownPartialSigs),
|
||||
maybeNonceSecret,
|
||||
)
|
||||
}
|
||||
|
||||
func eventToCliArgs(evt *nostr.Event) string {
|
||||
b := strings.Builder{}
|
||||
b.Grow(100)
|
||||
|
||||
b.WriteString("-k ")
|
||||
b.WriteString(strconv.Itoa(evt.Kind))
|
||||
|
||||
b.WriteString(" -ts ")
|
||||
b.WriteString(strconv.FormatInt(int64(evt.CreatedAt), 10))
|
||||
|
||||
b.WriteString(" -c '")
|
||||
b.WriteString(evt.Content)
|
||||
b.WriteString("'")
|
||||
|
||||
for _, tag := range evt.Tags {
|
||||
b.WriteString(" -t '")
|
||||
b.WriteString(tag.Key())
|
||||
if len(tag) > 1 {
|
||||
b.WriteString("=")
|
||||
b.WriteString(tag[1])
|
||||
if len(tag) > 2 {
|
||||
for _, item := range tag[2:] {
|
||||
b.WriteString(",")
|
||||
b.WriteString(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
b.WriteString("'")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func signersToCliArgs(knownSigners []*btcec.PublicKey) string {
|
||||
b := strings.Builder{}
|
||||
b.Grow(len(knownSigners) * (16 + 66))
|
||||
|
||||
for _, signerPub := range knownSigners {
|
||||
b.WriteString(" --musig-pubkey ")
|
||||
b.WriteString(hex.EncodeToString(signerPub.SerializeCompressed()))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func noncesToCliArgs(knownNonces [][66]byte) string {
|
||||
b := strings.Builder{}
|
||||
b.Grow(len(knownNonces) * (15 + 132))
|
||||
|
||||
for _, nonce := range knownNonces {
|
||||
b.WriteString(" --musig-nonce ")
|
||||
b.WriteString(hex.EncodeToString(nonce[:]))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func partialSigsToCliArgs(knownPartialSigs []*musig2.PartialSignature) string {
|
||||
b := strings.Builder{}
|
||||
b.Grow(len(knownPartialSigs) * (17 + 64))
|
||||
|
||||
for _, partialSig := range knownPartialSigs {
|
||||
b.WriteString(" --musig-partial ")
|
||||
w := &bytes.Buffer{}
|
||||
partialSig.Encode(w)
|
||||
b.Write([]byte(hex.EncodeToString(w.Bytes())))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// this function is copied from btcec because it's not exported for some reason
|
||||
func secNonceToPubNonce(secNonce [musig2.SecNonceSize]byte) [musig2.PubNonceSize]byte {
|
||||
var k1Mod, k2Mod btcec.ModNScalar
|
||||
k1Mod.SetByteSlice(secNonce[:btcec.PrivKeyBytesLen])
|
||||
k2Mod.SetByteSlice(secNonce[btcec.PrivKeyBytesLen:])
|
||||
|
||||
var r1, r2 btcec.JacobianPoint
|
||||
btcec.ScalarBaseMultNonConst(&k1Mod, &r1)
|
||||
btcec.ScalarBaseMultNonConst(&k2Mod, &r2)
|
||||
|
||||
// Next, we'll convert the key in jacobian format to a normal public
|
||||
// key expressed in affine coordinates.
|
||||
r1.ToAffine()
|
||||
r2.ToAffine()
|
||||
r1Pub := btcec.NewPublicKey(&r1.X, &r1.Y)
|
||||
r2Pub := btcec.NewPublicKey(&r2.X, &r2.Y)
|
||||
|
||||
var pubNonce [musig2.PubNonceSize]byte
|
||||
|
||||
// The public nonces are serialized as: R1 || R2, where both keys are
|
||||
// serialized in compressed format.
|
||||
copy(pubNonce[:], r1Pub.SerializeCompressed())
|
||||
copy(
|
||||
pubNonce[btcec.PubKeyBytesLenCompressed:],
|
||||
r2Pub.SerializeCompressed(),
|
||||
)
|
||||
|
||||
return pubNonce
|
||||
}
|
||||
39
relay.go
Normal file
39
relay.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr/nip11"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var relay = &cli.Command{
|
||||
Name: "relay",
|
||||
Usage: "gets the relay information document for the given relay, as JSON",
|
||||
Description: `example:
|
||||
nak relay nostr.wine`,
|
||||
ArgsUsage: "<relay-url>",
|
||||
Action: func(c *cli.Context) error {
|
||||
for url := range getStdinLinesOrArguments(c.Args()) {
|
||||
if url == "" {
|
||||
return fmt.Errorf("specify the <relay-url>")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(url, "wss://") && !strings.HasPrefix(url, "ws://") {
|
||||
url = "wss://" + url
|
||||
}
|
||||
|
||||
info, err := nip11.Fetch(c.Context, url)
|
||||
if err != nil {
|
||||
lineProcessingError(c, "failed to fetch '%s' information document: %w", url, err)
|
||||
continue
|
||||
}
|
||||
|
||||
pretty, _ := json.MarshalIndent(info, "", " ")
|
||||
stdout(string(pretty))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
141
req.go
141
req.go
@@ -3,8 +3,11 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -23,8 +26,7 @@ example:
|
||||
it can also take a filter from stdin, optionally modify it with flags and send it to specific relays (or just print it).
|
||||
|
||||
example:
|
||||
echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net
|
||||
`,
|
||||
echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "author",
|
||||
@@ -60,13 +62,18 @@ example:
|
||||
Usage: "shortcut for --tag p=<value>",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "d",
|
||||
Usage: "shortcut for --tag d=<value>",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "since",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "only accept events newer than this (unix timestamp)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
&cli.StringFlag{
|
||||
Name: "until",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "only accept events older than this (unix timestamp)",
|
||||
@@ -83,22 +90,93 @@ example:
|
||||
Usage: "a NIP-50 search query, use it only with relays that explicitly support it",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "bare",
|
||||
Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "stream",
|
||||
Usage: "keep the subscription open, printing all events as they are returned",
|
||||
DefaultText: "false, will close on EOSE",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "bare",
|
||||
Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "auth",
|
||||
Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "sec",
|
||||
Usage: "secret key to sign the AUTH challenge, 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 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...]",
|
||||
Action: func(c *cli.Context) error {
|
||||
var pool *nostr.SimplePool
|
||||
relayUrls := c.Args().Slice()
|
||||
if len(relayUrls) > 0 {
|
||||
var relays []*nostr.Relay
|
||||
pool, relays = connectToAllRelays(c.Context, relayUrls, nostr.WithAuthHandler(func(evt *nostr.Event) error {
|
||||
if !c.Bool("auth") {
|
||||
return fmt.Errorf("auth not authorized")
|
||||
}
|
||||
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if bunker != nil {
|
||||
return bunker.SignEvent(c.Context, evt)
|
||||
} else {
|
||||
return evt.Sign(sec)
|
||||
}
|
||||
}))
|
||||
if len(relays) == 0 {
|
||||
log("failed to connect to any of the given relays.\n")
|
||||
os.Exit(3)
|
||||
}
|
||||
relayUrls = make([]string, len(relays))
|
||||
for i, relay := range relays {
|
||||
relayUrls[i] = relay.URL
|
||||
}
|
||||
|
||||
defer func() {
|
||||
for _, relay := range relays {
|
||||
relay.Close()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for stdinFilter := range getStdinLinesOrBlank() {
|
||||
filter := nostr.Filter{}
|
||||
if stdinFilter := getStdin(); stdinFilter != "" {
|
||||
if err := json.Unmarshal([]byte(stdinFilter), &filter); err != nil {
|
||||
return fmt.Errorf("invalid filter received from stdin: %w", err)
|
||||
if stdinFilter != "" {
|
||||
if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil {
|
||||
lineProcessingError(c, "invalid filter '%s' received from stdin: %s", stdinFilter, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +207,9 @@ example:
|
||||
for _, ptag := range c.StringSlice("p") {
|
||||
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 {
|
||||
filter.Tags = make(nostr.TagMap)
|
||||
@@ -141,27 +222,41 @@ example:
|
||||
filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1])
|
||||
}
|
||||
|
||||
if since := c.Int("since"); since != 0 {
|
||||
ts := nostr.Timestamp(since)
|
||||
if since := c.String("since"); since != "" {
|
||||
if since == "now" {
|
||||
ts := nostr.Now()
|
||||
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
|
||||
} 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 {
|
||||
filter.Limit = limit
|
||||
} else if c.IsSet("limit") || c.Bool("stream") {
|
||||
filter.LimitZero = true
|
||||
}
|
||||
|
||||
relays := c.Args().Slice()
|
||||
if len(relays) > 0 {
|
||||
pool := nostr.NewSimplePool(c.Context)
|
||||
if len(relayUrls) > 0 {
|
||||
fn := pool.SubManyEose
|
||||
if c.Bool("stream") {
|
||||
fn = pool.SubMany
|
||||
}
|
||||
for ie := range fn(c.Context, relays, nostr.Filters{filter}) {
|
||||
fmt.Println(ie.Event)
|
||||
for ie := range fn(c.Context, relayUrls, nostr.Filters{filter}) {
|
||||
stdout(ie.Event)
|
||||
}
|
||||
} else {
|
||||
// no relays given, will just print the filter
|
||||
@@ -169,13 +264,15 @@ example:
|
||||
if c.Bool("bare") {
|
||||
result = filter.String()
|
||||
} else {
|
||||
j, _ := json.Marshal([]any{"REQ", "nak", filter})
|
||||
j, _ := json.Marshal(nostr.ReqEnvelope{SubscriptionID: "nak", Filters: nostr.Filters{filter}})
|
||||
result = string(j)
|
||||
}
|
||||
|
||||
fmt.Println(result)
|
||||
stdout(result)
|
||||
}
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(c)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
18
verify.go
18
verify.go
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/urfave/cli/v2"
|
||||
@@ -14,24 +13,29 @@ var verify = &cli.Command{
|
||||
Description: `example:
|
||||
echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak verify
|
||||
|
||||
it outputs nothing if the verification is successful.
|
||||
`,
|
||||
it outputs nothing if the verification is successful.`,
|
||||
Action: func(c *cli.Context) error {
|
||||
for stdinEvent := range getStdinLinesOrArguments(c.Args()) {
|
||||
evt := nostr.Event{}
|
||||
if stdinEvent := getStdin(); stdinEvent != "" {
|
||||
if stdinEvent != "" {
|
||||
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
||||
return fmt.Errorf("invalid JSON: %w", err)
|
||||
lineProcessingError(c, "invalid event: %s", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if evt.GetID() != evt.ID {
|
||||
return fmt.Errorf("invalid .id, expected %s, got %s", evt.GetID(), evt.ID)
|
||||
lineProcessingError(c, "invalid .id, expected %s, got %s", evt.GetID(), evt.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
if ok, err := evt.CheckSignature(); !ok {
|
||||
return fmt.Errorf("invalid signature: %w", err)
|
||||
lineProcessingError(c, "invalid signature: %s", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(c)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user