mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-12 18:18:50 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bce92f56d | ||
|
|
be8e3dfb39 | ||
|
|
e681ad87cd | ||
|
|
9e5e736395 | ||
|
|
c5573410df |
20
.github/workflows/publish-webapp.yml
vendored
20
.github/workflows/publish-webapp.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: build page and publish to cloudflare
|
||||
on:
|
||||
push:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- uses: olafurpg/setup-scala@v11
|
||||
- name: build page / compile scalajs
|
||||
run: sbt fullLinkJS/esBuild
|
||||
- name: publish to cloudflare
|
||||
uses: cloudflare/pages-action@v1
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: 60325047cc7d0811c6b337717918cbc1
|
||||
projectName: nostr-army-knife
|
||||
directory: .
|
||||
3
.github/workflows/release-cli.yml
vendored
3
.github/workflows/release-cli.yml
vendored
@@ -37,3 +37,6 @@ jobs:
|
||||
goos: ${{ matrix.goos }}
|
||||
goarch: ${{ matrix.goarch }}
|
||||
overwrite: true
|
||||
md5sum: false
|
||||
sha256sum: false
|
||||
compress_assets: false
|
||||
|
||||
267
README.md
267
README.md
@@ -1,225 +1,78 @@
|
||||
# nostr army knife
|
||||
# nak, the nostr army knife
|
||||
|
||||
this repository contains two things:
|
||||
install with `go install github.com/fiatjaf/nak@latest` or
|
||||
[download a binary](https://github.com/fiatjaf/nak/releases).
|
||||
|
||||
## a command-line tool for decoding and encoding nostr entities and talking to relays
|
||||
## what can you do with it?
|
||||
|
||||
Install with `go install github.com/fiatjaf/nak`.
|
||||
take a look at the help text that comes in it to learn all possibilities, but here are some:
|
||||
|
||||
It pairs nicely with https://github.com/blakejakopovic/nostcat using unix pipes.
|
||||
|
||||
### examples
|
||||
### make a nostr event signed with the default key (`01`)
|
||||
|
||||
```shell
|
||||
~> nak event
|
||||
{"id":"53443506e7d09e55b922a2369b80f926007a8a8a8ea5f09df1db59fe1993335e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1698632644,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"4bdb609c975b2b61338c2ff4c7ce91d4afe74bea4ed1601a62e1fd125bd4c0ae6e0166cca96e5cfb7e0f50583eb6a0dd0b66072566299b6007742db56278010c"}
|
||||
```
|
||||
~> nak decode nsec1aqc5q5l8da0l7u6gra4p5xhleclngezlpsgd7z5dx07cpu8sxf2shqgn6y
|
||||
{
|
||||
"pubkey": "5b36b874b2b983197ba4be80553b2e4b6db2895a04567cea0aa47585b2e0c620",
|
||||
"private_key": "e8314053e76f5fff73481f6a1a1affce3f34645f0c10df0a8d33fd80f0f03255"
|
||||
}
|
||||
|
||||
~> nak event -c hello --sec e8314053e76f5fff73481f6a1a1affce3f34645f0c10df0a8d33fd80f0f03255
|
||||
{"id":"ed840ef37a40cce4f4b8c361e5df13457ad664209cf4a297fd7df7e84fdd32e0","pubkey":"5b36b874b2b983197ba4be80553b2e4b6db2895a04567cea0aa47585b2e0c620","created_at":1683201092,"kind":1,"tags":[],"content":"hello","sig":"304a87dbbdf986a187eb9417316cfe3d6f8f31793ba20c9c6d7e4ebeeefe950d6ecba6098c201b7170c04e27c2f920d607a90f5c8763c35ac806dce37df1d05d"}
|
||||
~> nak event -c hello --sec e8314053e76f5fff73481f6a1a1affce3f34645f0c10df0a8d33fd80f0f03255 wss://relay.stoner.com wss://nos.lol wss://nostr.wine wss://atlas.nostr.land wss://relay.damus.io
|
||||
{"id":"54a534647bdcd2751d743fea4fc9eee5dba613887d69425f0891d9c2f82772a5","pubkey":"5b36b874b2b983197ba4be80553b2e4b6db2895a04567cea0aa47585b2e0c620","created_at":1684895417,"kind":1,"tags":[],"content":"hello","sig":"81a14cfe628fab6cd6135bb66f6e8b3bb4bfce4f666462a1303fdfbc9038fd141e73db3fe7e774a8f023fc70622c50a67d4fa41d3d09806c78f051985c11e0bd"}
|
||||
publishing to wss://relay.stoner.com... failed: msg: blocked: pubkey is not allowed to publish to this relay
|
||||
publishing to wss://nos.lol... success.
|
||||
publishing to wss://nostr.wine... failed: msg: blocked: not an active paid member
|
||||
publishing to wss://atlas.nostr.land... failed: msg: blocked: pubkey not admitted
|
||||
### 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
|
||||
{"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.
|
||||
```
|
||||
|
||||
~> nak decode nevent1qqs29yet5tp0qq5xu5qgkeehkzqh5qu46739axzezcxpj4tjlkx9j7gpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5sh59ud
|
||||
### 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
|
||||
"#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"
|
||||
"The problem is to start, but along the way it's fixed #GM ☀️"
|
||||
"Activando modo zen…\n\n#GM #Nostr #Hispano"
|
||||
```
|
||||
|
||||
### 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
|
||||
nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7
|
||||
~> nak decode nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7
|
||||
{
|
||||
"id": "a2932ba2c2f00286e5008b6737b0817a0395d7a25e9859160c195572fd8c5979",
|
||||
"id": "5ae731bbc7711f78513da14927c48cc7143a91e6cad0565fdc4d73b8967a7d59",
|
||||
"relays": [
|
||||
"wss://nostr-pub.wellorder.net"
|
||||
"wss://nostr.zbd.gg"
|
||||
]
|
||||
}
|
||||
|
||||
~> nak req -a a2932ba2c2f00286e5008b6737b0817a0395d7a25e9859160c195572fd8c5979 -k 1 -a e8b487c079b0f67c695ae6c4c2552a47f38adfa2533cc5926bd2c102942fdcb7
|
||||
["REQ","nak",{"kinds":[1],"authors":["a2932ba2c2f00286e5008b6737b0817a0395d7a25e9859160c195572fd8c5979","e8b487c079b0f67c695ae6c4c2552a47f38adfa2533cc5926bd2c102942fdcb7"]}]
|
||||
|
||||
~> nak req -k 1 -l 1 --stream wss://relay.stoner.com
|
||||
{"id":"1d73832917bf5a72276c53e9246c28b97225b51cd5735843434f7756fc0ddead","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1684894689,"kind":1,"tags":[["p","bcbeb5a2e6b547f6d0c3d8c16145f7bb94f3639ec7ecbcfe50045dbb2eede70b","wss://nos.lol","artk42"],["e","b5af6815c8d89a7d5b6201b9e624fbd5389fca3337ba2dc05c6187234a7c1bd5","wss://nos.lol","root"],["e","5795e27aff0a459a30c64a61a32c43d968cd19c8f1926cf01fc02e9da7c56f2b","wss://nos.lol","reply"],["client","coracle"]],"content":"Because that makes no sense.","sig":"3ee5b2b26ec6b116ef1a6b1c10bc7e56674a3c36841814f68b57f63259f3d78e23629d4599afe67e72c220e27b4b0966cc51adc1da808c8c6111dedb531ac0c3"}
|
||||
```
|
||||
|
||||
### documentation
|
||||
|
||||
```
|
||||
~> nak --help
|
||||
NAME:
|
||||
nak - the nostr army knife command-line tool
|
||||
|
||||
USAGE:
|
||||
nak [global options] command [command options] [arguments...]
|
||||
|
||||
COMMANDS:
|
||||
req generates encoded REQ messages and optionally use them to talk to relays
|
||||
count generates encoded COUNT messages and optionally use them to talk to relays
|
||||
event generates an encoded event and either prints it or sends it to a set of relays
|
||||
decode decodes nip19, nip21, nip05 or hex entities
|
||||
encode encodes notes and other stuff to nip19 entities
|
||||
help, h Shows a list of commands or help for one command
|
||||
|
||||
GLOBAL OPTIONS:
|
||||
--help, -h show help
|
||||
|
||||
~> nak event --help
|
||||
NAME:
|
||||
nak event - generates an encoded event and either prints it or sends it to a set of relays
|
||||
|
||||
USAGE:
|
||||
nak event [command options] [arguments...]
|
||||
|
||||
DESCRIPTION:
|
||||
example usage (for sending directly to a relay with 'nostcat'):
|
||||
nak event -k 1 -c hello --envelope | nostcat wss://nos.lol
|
||||
standalone:
|
||||
nak event -k 1 -c hello wss://nos.lol`,
|
||||
|
||||
OPTIONS:
|
||||
--envelope print the event enveloped in a ["EVENT", ...] message ready to be sent to a relay (default: false)
|
||||
--sec value secret key to sign the event (default: the key '1')
|
||||
|
||||
EVENT FIELDS
|
||||
|
||||
--content value, -c value event content (default: hello from the nostr army knife)
|
||||
--created-at value, --time value, --ts value unix timestamp value for the created_at field (default: now)
|
||||
--kind value, -k value event kind (default: 1)
|
||||
--tag value, -t value [ --tag value, -t value ] sets a tag field on the event, takes a value like -t e=<id>
|
||||
-e value [ -e value ] shortcut for --tag e=<value>
|
||||
-p value [ -p value ] shortcut for --tag p=<value>
|
||||
|
||||
~> nak req --help
|
||||
NAME:
|
||||
nak req - generates encoded REQ messages and optionally use them to talk to relays
|
||||
|
||||
USAGE:
|
||||
nak req [command options] [relay...]
|
||||
|
||||
DESCRIPTION:
|
||||
outputs a NIP-01 Nostr filter. when a relay is not given, will print the filter, otherwise will connect to the given relay and send the filter.
|
||||
|
||||
example usage (with 'nostcat'):
|
||||
nak req -k 1 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d | nostcat wss://nos.lol
|
||||
standalone:
|
||||
nak req -k 1 wss://nos.lol
|
||||
|
||||
OPTIONS:
|
||||
--bare when printing the filter, print just the filter, not enveloped in a ["REQ", ...] array (default: false)
|
||||
--stream keep the subscription open, printing all events as they are returned (default: false, will close on EOSE)
|
||||
|
||||
FILTER ATTRIBUTES
|
||||
|
||||
--author value, -a value [ --author value, -a value ] only accept events from these authors (pubkey as hex)
|
||||
--id value, -i value [ --id value, -i value ] only accept events with these ids (hex)
|
||||
--kind value, -k value [ --kind value, -k value ] only accept events with these kind numbers
|
||||
--limit value, -l value only accept up to this number of events (default: 0)
|
||||
--since value, -s value only accept events newer than this (unix timestamp) (default: 0)
|
||||
--tag value, -t value [ --tag value, -t value ] takes a tag like -t e=<id>, only accept events with these tags
|
||||
--until value, -u value only accept events older than this (unix timestamp) (default: 0)
|
||||
-e value [ -e value ] shortcut for --tag e=<value>
|
||||
-p value [ -p value ] shortcut for --tag p=<value>
|
||||
|
||||
|
||||
OPTIONS:
|
||||
--bare when printing the filter, print just the filter, not enveloped in a ["REQ", ...] array (default: false)
|
||||
--stream keep the subscription open, printing all events as they are returned (default: false, will close on EOSE)
|
||||
|
||||
FILTER ATTRIBUTES
|
||||
|
||||
--author value, -a value [ --author value, -a value ] only accept events from these authors (pubkey as hex)
|
||||
--id value, -i value [ --id value, -i value ] only accept events with these ids (hex)
|
||||
--kind value, -k value [ --kind value, -k value ] only accept events with these kind numbers
|
||||
--limit value, -l value only accept up to this number of events (default: 0)
|
||||
--since value, -s value only accept events newer than this (unix timestamp) (default: 0)
|
||||
--tag value, -t value [ --tag value, -t value ] takes a tag like -t e=<id>, only accept events with these tags
|
||||
--until value, -u value only accept events older than this (unix timestamp) (default: 0)
|
||||
-e value [ -e value ] shortcut for --tag e=<value>
|
||||
-p value [ -p value ] shortcut for --tag p=<value>
|
||||
|
||||
~> nak count --help
|
||||
NAME:
|
||||
nak count - generates encoded COUNT messages and optionally use them to talk to relays
|
||||
|
||||
USAGE:
|
||||
nak count [command options] [relay...]
|
||||
|
||||
DESCRIPTION:
|
||||
outputs a NIP-45 request. Mostly same as req.
|
||||
|
||||
example usage (with 'nostcat'):
|
||||
nak count -k 1 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d | nostcat wss://nos.lol
|
||||
standalone:
|
||||
nak count -k 1 wss://nos.lol
|
||||
|
||||
OPTIONS:
|
||||
--bare when printing the filter, print just the filter, not enveloped in a ["COUNT", ...] array (default: false)
|
||||
--stream keep the subscription open, printing all events as they are returned (default: false, will close on EOSE)
|
||||
|
||||
FILTER ATTRIBUTES
|
||||
|
||||
--author value, -a value [ --author value, -a value ] only accept events from these authors (pubkey as hex)
|
||||
--id value, -i value [ --id value, -i value ] only accept events with these ids (hex)
|
||||
--kind value, -k value [ --kind value, -k value ] only accept events with these kind numbers
|
||||
--limit value, -l value only accept up to this number of events (default: 0)
|
||||
--since value, -s value only accept events newer than this (unix timestamp) (default: 0)
|
||||
--tag value, -t value [ --tag value, -t value ] takes a tag like -t e=<id>, only accept events with these tags
|
||||
--until value, -u value only accept events older than this (unix timestamp) (default: 0)
|
||||
-e value [ -e value ] shortcut for --tag e=<value>
|
||||
-p value [ -p value ] shortcut for --tag p=<value>
|
||||
|
||||
~> nak decode --help
|
||||
NAME:
|
||||
nak decode - decodes nip19, nip21, nip05 or hex entities
|
||||
|
||||
USAGE:
|
||||
nak decode [command options] <npub | nprofile | nip05 | nevent | naddr | nsec>
|
||||
|
||||
DESCRIPTION:
|
||||
example usage:
|
||||
nak decode npub1uescmd5krhrmj9rcura833xpke5eqzvcz5nxjw74ufeewf2sscxq4g7chm
|
||||
nak decode nevent1qqs29yet5tp0qq5xu5qgkeehkzqh5qu46739axzezcxpj4tjlkx9j7gpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5sh59ud
|
||||
nak decode nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpz4mhxue69uhk2er9dchxummnw3ezumrpdejqz8thwden5te0dehhxarj94c82c3wwajkcmr0wfjx2u3wdejhgqgcwaehxw309aex2mrp0yhxummnw3exzarf9e3k7mgnp0sh5
|
||||
nak decode nsec1jrmyhtjhgd9yqalps8hf9mayvd58852gtz66m7tqpacjedkp6kxq4dyxsr
|
||||
|
||||
OPTIONS:
|
||||
--id, -e return just the event id, if applicable (default: false)
|
||||
--pubkey, -p return just the pubkey, if applicable (default: false)
|
||||
--help, -h show help
|
||||
|
||||
|
||||
~> nak encode --help
|
||||
NAME:
|
||||
nak encode - encodes notes and other stuff to nip19 entities
|
||||
|
||||
USAGE:
|
||||
nak encode command [command options] [arguments...]
|
||||
|
||||
DESCRIPTION:
|
||||
example usage:
|
||||
nak encode npub <pubkey-hex>
|
||||
nak encode nprofile <pubkey-hex>
|
||||
nak encode nprofile --relay <relay-url> <pubkey-hex>
|
||||
nak encode nevent <event-id>
|
||||
nak encode nevent --author <pubkey-hex> --relay <relay-url> --relay <other-relay> <event-id>
|
||||
nak encode nsec <privkey-hex>
|
||||
|
||||
COMMANDS:
|
||||
npub encode a hex private key into bech32 'npub' format
|
||||
nsec encode a hex private key into bech32 'nsec' format
|
||||
nprofile generate profile codes with attached relay information
|
||||
nevent generate event codes with optionally attached relay information
|
||||
naddr generate codes for NIP-33 parameterized replaceable events
|
||||
help, h Shows a list of commands or help for one command
|
||||
|
||||
OPTIONS:
|
||||
--help, -h show help
|
||||
### fetch an event using relay and author hints automatically from a nevent1 code, pretty-print it
|
||||
```shell
|
||||
nak fetch nevent1qqs2e3k48vtrkzjm8vvyzcmsmkf58unrxtq2k4h5yspay6vhcqm4wqcpz9mhxue69uhkummnw3ezuamfdejj7q3ql2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqxpqqqqqqz7ttjyq | jq
|
||||
{
|
||||
"id": "acc6d53b163b0a5b3b18416370dd9343f26332c0ab56f42403d26997c0375703",
|
||||
"pubkey": "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
|
||||
"created_at": 1697370933,
|
||||
"kind": 1,
|
||||
"tags": [],
|
||||
"content": "`q` tags = a kind 1 that wanted to be a kind:6 but fell short\n\n🥁",
|
||||
"sig": "b5b63d7c8491a4a0517df2c58151665c583abc6cd31fd50b957bf8fefc8e55c87c922cbdcb50888cb9f1c03c26ab5c02c1dccc14b46b78e1e16c60094f2358da"
|
||||
}
|
||||
```
|
||||
|
||||
written in go using [go-nostr](https://github.com/nbd-wtf/go-nostr), heavily inspired by [nostril](http://git.jb55.com/nostril/).
|
||||
### 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
|
||||
{"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.
|
||||
publishing to wss://public.relaying.io... success.
|
||||
publishing to wss://eden.nostr.land... failed: msg: blocked: not on white-list
|
||||
publishing to wss://atlas.nostr.land... failed: msg: blocked: not on white-list
|
||||
publishing to wss://relayable.org... success.
|
||||
```
|
||||
|
||||
## a toolkit for debugging all things nostr as a webpage:
|
||||
|
||||

|
||||
|
||||
written in [scala](https://scala-lang.org/) with [calico](https://www.armanbilge.com/calico/) and [snow](https://github.com/fiatjaf/snow)
|
||||
### verify if an event is good
|
||||
```shell
|
||||
~> echo '{"content":"hello world","created_at":1698923350,"id":"05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a","kind":1,"pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","sig":"0a04a296321ed933858577f36fb2fb9a0933e966f9ee32b539493f5a4d00120891b1ca9152ebfbc04fb403bdaa7c73f415e7c4954e55726b4b4fa8cebf008cd6","tags":[]}' | nak verify
|
||||
invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a, got 05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a
|
||||
```
|
||||
|
||||
14
build.sbt
14
build.sbt
@@ -1,14 +0,0 @@
|
||||
enablePlugins(ScalaJSPlugin, EsbuildPlugin)
|
||||
|
||||
name := "nostr-army-knife"
|
||||
scalaVersion := "3.3.0-RC4"
|
||||
|
||||
lazy val root = (project in file("."))
|
||||
.settings(
|
||||
libraryDependencies ++= Seq(
|
||||
"com.armanbilge" %%% "calico" % "0.2.0-RC2",
|
||||
"com.fiatjaf" %%% "snow" % "0.0.1"
|
||||
),
|
||||
scalaJSUseMainModuleInitializer := true,
|
||||
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }
|
||||
)
|
||||
7
edit.svg
7
edit.svg
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" ?>
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path d="M20,16v4a2,2,0,0,1-2,2H4a2,2,0,0,1-2-2V6A2,2,0,0,1,4,4H8" fill="none" stroke="#f9cc9d" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<polygon fill="none" points="12.5 15.8 22 6.2 17.8 2 8.3 11.5 8 16 12.5 15.8" stroke="#f9cc9d" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 463 B |
1
ext.svg
1
ext.svg
@@ -1 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17 13.5v6H5v-12h6m3-3h6v6m0-6-9 9" class="icon_svg-stroke" stroke="#3b82f6" stroke-width="1.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
||||
|
Before Width: | Height: | Size: 281 B |
BIN
favicon.ico
BIN
favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB |
@@ -1,7 +0,0 @@
|
||||
<meta charset=utf-8>
|
||||
<title>nostr army knife</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<body class="bg-emerald-200 text-black m-0 w-full h-full">
|
||||
<div id="app" class="w-full h-full"></div>
|
||||
<script type="module" src="/target/esbuild/bundle.js"></script>
|
||||
</body>
|
||||
13
justfile
13
justfile
@@ -1,13 +0,0 @@
|
||||
build-prod:
|
||||
sbt fullLinkJS/esBuild
|
||||
|
||||
cloudflare:
|
||||
rm -fr cf
|
||||
mkdir -p cf/target/esbuild
|
||||
cp index.html cf/
|
||||
cp favicon.ico cf/
|
||||
cp target/esbuild/bundle.js cf/target/esbuild
|
||||
wrangler pages publish cf --project-name nostr-army-knife --branch master
|
||||
rm -fr cf
|
||||
|
||||
build-and-deploy: build-prod cloudflare
|
||||
@@ -1 +0,0 @@
|
||||
sbt.version=1.7.1
|
||||
@@ -1,2 +0,0 @@
|
||||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.0")
|
||||
addSbtPlugin("com.fiatjaf" % "sbt-esbuild" % "0.1.1")
|
||||
@@ -1,476 +0,0 @@
|
||||
import cats.data.{Store => *, *}
|
||||
import cats.effect.*
|
||||
import cats.effect.syntax.all.*
|
||||
import cats.syntax.all.*
|
||||
import fs2.concurrent.*
|
||||
import fs2.dom.{Event => _, *}
|
||||
import io.circe.parser.*
|
||||
import io.circe.syntax.*
|
||||
import calico.*
|
||||
import calico.html.io.{*, given}
|
||||
import calico.syntax.*
|
||||
import scodec.bits.ByteVector
|
||||
import scoin.*
|
||||
import snow.*
|
||||
|
||||
import Utils.*
|
||||
|
||||
object Components {
|
||||
def render32Bytes(
|
||||
store: Store,
|
||||
bytes32: ByteVector32
|
||||
): Resource[IO, HtmlDivElement[IO]] =
|
||||
div(
|
||||
cls := "text-md",
|
||||
entry("canonical hex", bytes32.toHex),
|
||||
"if this is a public key:",
|
||||
div(
|
||||
cls := "mt-2 pl-2 mb-2",
|
||||
entry(
|
||||
"npub",
|
||||
NIP19.encode(XOnlyPublicKey(bytes32)),
|
||||
Some(
|
||||
selectable(
|
||||
store,
|
||||
NIP19.encode(XOnlyPublicKey(bytes32))
|
||||
)
|
||||
)
|
||||
),
|
||||
nip19_21(
|
||||
store,
|
||||
"nprofile",
|
||||
NIP19.encode(ProfilePointer(XOnlyPublicKey(bytes32)))
|
||||
)
|
||||
),
|
||||
"if this is a private key:",
|
||||
div(
|
||||
cls := "pl-2 mb-2",
|
||||
entry(
|
||||
"nsec",
|
||||
NIP19.encode(PrivateKey(bytes32)),
|
||||
Some(
|
||||
selectable(
|
||||
store,
|
||||
NIP19.encode(PrivateKey(bytes32))
|
||||
)
|
||||
)
|
||||
),
|
||||
entry(
|
||||
"npub",
|
||||
NIP19.encode(PrivateKey(bytes32).publicKey.xonly),
|
||||
Some(
|
||||
selectable(
|
||||
store,
|
||||
NIP19.encode(PrivateKey(bytes32).publicKey.xonly)
|
||||
)
|
||||
)
|
||||
),
|
||||
nip19_21(
|
||||
store,
|
||||
"nprofile",
|
||||
NIP19.encode(ProfilePointer(PrivateKey(bytes32).publicKey.xonly))
|
||||
)
|
||||
),
|
||||
"if this is an event id:",
|
||||
div(
|
||||
cls := "pl-2 mb-2",
|
||||
nip19_21(
|
||||
store,
|
||||
"nevent",
|
||||
NIP19.encode(EventPointer(bytes32.toHex))
|
||||
)
|
||||
),
|
||||
div(
|
||||
cls := "pl-2 mb-2",
|
||||
entry(
|
||||
"note",
|
||||
NIP19.encode(bytes32),
|
||||
Some(
|
||||
selectable(
|
||||
store,
|
||||
NIP19.encode(bytes32)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def renderEventPointer(
|
||||
store: Store,
|
||||
evp: snow.EventPointer
|
||||
): Resource[IO, HtmlDivElement[IO]] =
|
||||
div(
|
||||
cls := "text-md",
|
||||
entry(
|
||||
"event id (hex)",
|
||||
evp.id,
|
||||
Some(selectable(store, evp.id))
|
||||
),
|
||||
relayHints(store, evp.relays),
|
||||
evp.author.map { pk =>
|
||||
entry("author hint (pubkey hex)", pk.value.toHex)
|
||||
},
|
||||
nip19_21(store, "nevent", NIP19.encode(evp)),
|
||||
entry(
|
||||
"note",
|
||||
NIP19.encode(ByteVector32.fromValidHex(evp.id)),
|
||||
Some(selectable(store, NIP19.encode(ByteVector32.fromValidHex(evp.id))))
|
||||
)
|
||||
)
|
||||
|
||||
def renderProfilePointer(
|
||||
store: Store,
|
||||
pp: snow.ProfilePointer,
|
||||
sk: Option[PrivateKey] = None
|
||||
): Resource[IO, HtmlDivElement[IO]] =
|
||||
div(
|
||||
cls := "text-md",
|
||||
sk.map { k =>
|
||||
entry(
|
||||
"private key (hex)",
|
||||
k.value.toHex,
|
||||
Some(selectable(store, k.value.toHex))
|
||||
)
|
||||
},
|
||||
sk.map { k =>
|
||||
entry(
|
||||
"nsec",
|
||||
NIP19.encode(k),
|
||||
Some(selectable(store, NIP19.encode(k)))
|
||||
)
|
||||
},
|
||||
entry(
|
||||
"public key (hex)",
|
||||
pp.pubkey.value.toHex,
|
||||
Some(selectable(store, pp.pubkey.value.toHex))
|
||||
),
|
||||
relayHints(
|
||||
store,
|
||||
pp.relays,
|
||||
dynamic = if sk.isDefined then false else true
|
||||
),
|
||||
entry(
|
||||
"npub",
|
||||
NIP19.encode(pp.pubkey),
|
||||
Some(selectable(store, NIP19.encode(pp.pubkey)))
|
||||
),
|
||||
nip19_21(store, "nprofile", NIP19.encode(pp))
|
||||
)
|
||||
|
||||
def renderAddressPointer(
|
||||
store: Store,
|
||||
addr: snow.AddressPointer
|
||||
): Resource[IO, HtmlDivElement[IO]] = {
|
||||
val nip33atag =
|
||||
s"${addr.kind}:${addr.author.value.toHex}:${addr.d}"
|
||||
|
||||
div(
|
||||
cls := "text-md",
|
||||
entry("author (pubkey hex)", addr.author.value.toHex),
|
||||
entry("identifier (d tag)", addr.d),
|
||||
entry("kind", addr.kind.toString),
|
||||
relayHints(store, addr.relays),
|
||||
nip19_21(store, "naddr", NIP19.encode(addr)),
|
||||
entry("nip33 'a' tag", nip33atag, Some(selectable(store, nip33atag)))
|
||||
)
|
||||
}
|
||||
|
||||
def renderEvent(
|
||||
store: Store,
|
||||
event: Event
|
||||
): Resource[IO, HtmlDivElement[IO]] =
|
||||
div(
|
||||
cls := "text-md",
|
||||
if event.pubkey.isEmpty then
|
||||
Some(
|
||||
div(
|
||||
cls := "flex items-center",
|
||||
entry("missing", "pubkey"),
|
||||
button(
|
||||
Styles.buttonSmall,
|
||||
"fill with a debugging key",
|
||||
onClick --> (_.foreach { _ =>
|
||||
store.input.set(
|
||||
event
|
||||
.copy(pubkey = Some(keyOne.publicKey.xonly))
|
||||
.asJson
|
||||
.printWith(jsonPrinter)
|
||||
)
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
else None,
|
||||
if event.id.isEmpty then
|
||||
Some(
|
||||
div(
|
||||
cls := "flex items-center",
|
||||
entry("missing", "id"),
|
||||
if event.pubkey.isDefined then
|
||||
Some(
|
||||
button(
|
||||
Styles.buttonSmall,
|
||||
"fill id",
|
||||
onClick --> (_.foreach(_ =>
|
||||
store.input.set(
|
||||
event
|
||||
.copy(id = Some(event.hash.toHex))
|
||||
.asJson
|
||||
.printWith(jsonPrinter)
|
||||
)
|
||||
))
|
||||
)
|
||||
)
|
||||
else None
|
||||
)
|
||||
)
|
||||
else None,
|
||||
if event.sig.isEmpty then
|
||||
Some(
|
||||
div(
|
||||
cls := "flex items-center",
|
||||
entry("missing", "sig"),
|
||||
if event.id.isDefined && event.pubkey == Some(
|
||||
keyOne.publicKey.xonly
|
||||
)
|
||||
then
|
||||
Some(
|
||||
button(
|
||||
Styles.buttonSmall,
|
||||
"sign",
|
||||
onClick --> (_.foreach(_ =>
|
||||
store.input.set(
|
||||
event
|
||||
.sign(keyOne)
|
||||
.asJson
|
||||
.printWith(jsonPrinter)
|
||||
)
|
||||
))
|
||||
)
|
||||
)
|
||||
else None
|
||||
)
|
||||
)
|
||||
else None,
|
||||
entry("serialized event", event.serialized),
|
||||
entry("implied event id", event.hash.toHex),
|
||||
entry(
|
||||
"does the implied event id match the given event id?",
|
||||
event.id == Some(event.hash.toHex) match {
|
||||
case true => "yes"; case false => "no"
|
||||
}
|
||||
),
|
||||
entry(
|
||||
"is signature valid?",
|
||||
event.isValid match {
|
||||
case true => "yes"; case false => "no"
|
||||
}
|
||||
),
|
||||
event.id.map(id =>
|
||||
nip19_21(
|
||||
store,
|
||||
"nevent",
|
||||
NIP19.encode(EventPointer(id, author = event.pubkey))
|
||||
)
|
||||
),
|
||||
if event.kind >= 30000 && event.kind < 40000 then
|
||||
event.pubkey
|
||||
.map(author =>
|
||||
nip19_21(
|
||||
store,
|
||||
"naddr",
|
||||
NIP19.encode(
|
||||
AddressPointer(
|
||||
d = event.tags
|
||||
.collectFirst { case "d" :: v :: _ => v }
|
||||
.getOrElse(""),
|
||||
kind = event.kind,
|
||||
author = author,
|
||||
relays = List.empty
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
else
|
||||
event.id.map(id =>
|
||||
entry(
|
||||
"note",
|
||||
NIP19.encode(ByteVector32.fromValidHex(id)),
|
||||
Some(selectable(store, NIP19.encode(ByteVector32.fromValidHex(id))))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
private def entry(
|
||||
key: String,
|
||||
value: String,
|
||||
selectLink: Option[Resource[IO, HtmlSpanElement[IO]]] = None
|
||||
): Resource[IO, HtmlDivElement[IO]] =
|
||||
div(
|
||||
cls := "flex items-center space-x-3",
|
||||
span(cls := "font-bold", key + " "),
|
||||
span(Styles.mono, cls := "max-w-xl break-all", value),
|
||||
selectLink
|
||||
)
|
||||
|
||||
private def nip19_21(
|
||||
store: Store,
|
||||
key: String,
|
||||
code: String
|
||||
): Resource[IO, HtmlDivElement[IO]] =
|
||||
div(
|
||||
span(cls := "font-bold", key + " "),
|
||||
span(Styles.mono, cls := "break-all", code),
|
||||
selectable(store, code),
|
||||
a(
|
||||
href := "nostr:" + code,
|
||||
external
|
||||
)
|
||||
)
|
||||
|
||||
private def relayHints(
|
||||
store: Store,
|
||||
relays: List[String],
|
||||
dynamic: Boolean = true
|
||||
): Resource[IO, HtmlDivElement[IO]] =
|
||||
if !dynamic && relays.isEmpty then div("")
|
||||
else
|
||||
SignallingRef[IO].of(false).toResource.flatMap { active =>
|
||||
val value =
|
||||
if relays.size > 0 then relays.reduce((a, b) => s"$a, $b") else ""
|
||||
|
||||
div(
|
||||
cls := "flex items-center space-x-3",
|
||||
span(cls := "font-bold", "relay hints "),
|
||||
if relays.size == 0 then div("")
|
||||
else
|
||||
// displaying each relay hint
|
||||
div(
|
||||
cls := "flex flex-wrap max-w-xl",
|
||||
relays
|
||||
.map(url =>
|
||||
div(
|
||||
Styles.mono,
|
||||
cls := "flex items-center rounded py-0.5 px-1 mr-1 mb-1 bg-orange-100",
|
||||
url,
|
||||
// removing a relay hint by clicking on the x
|
||||
div(
|
||||
cls := "cursor-pointer ml-1 text-rose-600 hover:text-rose-300",
|
||||
onClick --> (_.foreach(_ => {
|
||||
store.result.get.flatMap(result =>
|
||||
store.input.set(
|
||||
result
|
||||
.map {
|
||||
case a: AddressPointer =>
|
||||
NIP19
|
||||
.encode(
|
||||
a.copy(relays =
|
||||
relays.filterNot(_ == url)
|
||||
)
|
||||
)
|
||||
case p: ProfilePointer =>
|
||||
NIP19
|
||||
.encode(
|
||||
p.copy(relays =
|
||||
relays.filterNot(_ == url)
|
||||
)
|
||||
)
|
||||
case e: EventPointer =>
|
||||
NIP19
|
||||
.encode(
|
||||
e.copy(relays =
|
||||
relays.filterNot(_ == url)
|
||||
)
|
||||
)
|
||||
case r => ""
|
||||
}
|
||||
.getOrElse("")
|
||||
)
|
||||
)
|
||||
})),
|
||||
"×"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
,
|
||||
active.map {
|
||||
case true =>
|
||||
div(
|
||||
input.withSelf { self =>
|
||||
(
|
||||
onKeyPress --> (_.foreach(evt =>
|
||||
// confirm adding a relay hint
|
||||
evt.key match {
|
||||
case "Enter" =>
|
||||
self.value.get.flatMap(url =>
|
||||
if url.startsWith("wss://") || url
|
||||
.startsWith("ws://")
|
||||
then {
|
||||
store.result.get.flatMap(result =>
|
||||
store.input.set(
|
||||
result
|
||||
.map {
|
||||
case a: AddressPointer =>
|
||||
NIP19
|
||||
.encode(
|
||||
a.copy(relays = a.relays :+ url)
|
||||
)
|
||||
case p: ProfilePointer =>
|
||||
NIP19
|
||||
.encode(
|
||||
p.copy(relays = p.relays :+ url)
|
||||
)
|
||||
case e: EventPointer =>
|
||||
NIP19
|
||||
.encode(
|
||||
e.copy(relays = e.relays :+ url)
|
||||
)
|
||||
case r => ""
|
||||
}
|
||||
.getOrElse("")
|
||||
)
|
||||
)
|
||||
>> active.set(false)
|
||||
} else IO.unit
|
||||
)
|
||||
case _ => IO.unit
|
||||
}
|
||||
))
|
||||
)
|
||||
}
|
||||
)
|
||||
case false if dynamic =>
|
||||
// button to add a new relay hint
|
||||
button(
|
||||
Styles.buttonSmall,
|
||||
"add relay hint",
|
||||
onClick --> (_.foreach(_ => active.set(true)))
|
||||
)
|
||||
case false => div("")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private def selectable(
|
||||
store: Store,
|
||||
code: String
|
||||
): Resource[IO, HtmlSpanElement[IO]] =
|
||||
span(
|
||||
store.input.map(current =>
|
||||
if current == code then a("")
|
||||
else
|
||||
a(
|
||||
href := "#/" + code,
|
||||
onClick --> (_.foreach(evt =>
|
||||
evt.preventDefault >>
|
||||
store.input.set(code)
|
||||
)),
|
||||
edit
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
private val edit = img(cls := "inline w-4 ml-2", src := "edit.svg")
|
||||
private val external = img(cls := "inline w-4 ml-2", src := "ext.svg")
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import cats.effect.*
|
||||
import cats.effect.syntax.all.*
|
||||
import cats.syntax.all.*
|
||||
import fs2.concurrent.*
|
||||
import fs2.dom.{Event => _, *}
|
||||
import io.circe.parser.*
|
||||
import io.circe.syntax.*
|
||||
import calico.*
|
||||
import calico.html.io.{*, given}
|
||||
import calico.syntax.*
|
||||
import scoin.*
|
||||
import snow.*
|
||||
|
||||
import Utils.*
|
||||
import Components.*
|
||||
|
||||
object Main extends IOWebApp {
|
||||
def render: Resource[IO, HtmlDivElement[IO]] = Store(window).flatMap {
|
||||
store =>
|
||||
div(
|
||||
cls := "flex w-full flex-col items-center justify-center",
|
||||
div(
|
||||
cls := "w-4/5",
|
||||
h1(
|
||||
cls := "px-1 py-2 text-center text-xl",
|
||||
img(
|
||||
cls := "inline-block w-8 mr-2",
|
||||
src := "/favicon.ico"
|
||||
),
|
||||
a(
|
||||
href := "/",
|
||||
"nostr army knife"
|
||||
)
|
||||
),
|
||||
div(
|
||||
cls := "flex my-3",
|
||||
input(store),
|
||||
actions(store)
|
||||
),
|
||||
result(store)
|
||||
),
|
||||
div(
|
||||
cls := "flex justify-end mr-5 mt-10 text-xs w-4/5",
|
||||
a(
|
||||
href := "https://github.com/fiatjaf/nak",
|
||||
"source code"
|
||||
),
|
||||
a(
|
||||
cls := "ml-4",
|
||||
href := "https://github.com/fiatjaf/nak",
|
||||
"get the command-line tool"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def actions(store: Store): Resource[IO, HtmlDivElement[IO]] =
|
||||
div(
|
||||
cls := "flex flex-col space-y-1 my-3",
|
||||
store.input.map {
|
||||
case "" => div("")
|
||||
case _ =>
|
||||
button(
|
||||
Styles.button,
|
||||
"clear",
|
||||
onClick --> (_.foreach(_ => store.input.set("")))
|
||||
)
|
||||
},
|
||||
store.result.map {
|
||||
case Right(_: Event) =>
|
||||
button(
|
||||
Styles.button,
|
||||
"format",
|
||||
onClick --> (_.foreach(_ =>
|
||||
store.input.update(original =>
|
||||
parse(original).toOption
|
||||
.map(_.printWith(jsonPrinter))
|
||||
.getOrElse(original)
|
||||
)
|
||||
))
|
||||
)
|
||||
case _ => div("")
|
||||
},
|
||||
button(
|
||||
Styles.button,
|
||||
"generate event",
|
||||
onClick --> (_.foreach(_ =>
|
||||
store.input.set(
|
||||
Event(
|
||||
kind = 1,
|
||||
content = "hello world"
|
||||
).sign(keyOne)
|
||||
.asJson
|
||||
.printWith(jsonPrinter)
|
||||
)
|
||||
))
|
||||
),
|
||||
button(
|
||||
Styles.button,
|
||||
"generate keypair",
|
||||
onClick --> (_.foreach(_ =>
|
||||
store.input.set(
|
||||
NIP19.encode(PrivateKey(randomBytes32()))
|
||||
)
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
def input(store: Store): Resource[IO, HtmlDivElement[IO]] =
|
||||
div(
|
||||
cls := "w-full grow",
|
||||
div(
|
||||
cls := "w-full flex justify-center",
|
||||
textArea.withSelf { self =>
|
||||
(
|
||||
cls := "w-full max-h-96 p-3 rounded",
|
||||
styleAttr := "min-height: 280px; font-family: monospace",
|
||||
spellCheck := false,
|
||||
placeholder := "paste something nostric (event JSON, nprofile, npub, nevent etc or hex key or id)",
|
||||
onInput --> (_.foreach(_ =>
|
||||
self.value.get.flatMap(store.input.set)
|
||||
)),
|
||||
value <-- store.input
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def result(store: Store): Resource[IO, HtmlDivElement[IO]] =
|
||||
div(
|
||||
cls := "w-full flex my-5",
|
||||
store.result.map {
|
||||
case Left(msg) => div(msg)
|
||||
case Right(bytes: ByteVector32) => render32Bytes(store, bytes)
|
||||
case Right(event: Event) => renderEvent(store, event)
|
||||
case Right(pp: ProfilePointer) => renderProfilePointer(store, pp)
|
||||
case Right(evp: EventPointer) => renderEventPointer(store, evp)
|
||||
case Right(sk: PrivateKey) =>
|
||||
renderProfilePointer(
|
||||
store,
|
||||
ProfilePointer(pubkey = sk.publicKey.xonly),
|
||||
Some(sk)
|
||||
)
|
||||
case Right(addr: AddressPointer) => renderAddressPointer(store, addr)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import scala.util.Try
|
||||
import io.circe.parser.*
|
||||
import cats.syntax.all.*
|
||||
import scodec.bits.ByteVector
|
||||
import scoin.*
|
||||
import snow.*
|
||||
|
||||
type Result = Either[
|
||||
String,
|
||||
Event | PrivateKey | AddressPointer | EventPointer | ProfilePointer |
|
||||
ByteVector32
|
||||
]
|
||||
|
||||
object Parser {
|
||||
val additions = raw" *\+ *".r
|
||||
|
||||
def parseInput(input: String): Result =
|
||||
if input == "" then Left("")
|
||||
else
|
||||
ByteVector
|
||||
.fromHex(input)
|
||||
.flatMap(b => Try(Right(ByteVector32(b))).toOption)
|
||||
.getOrElse(
|
||||
NIP19.decode(input) match {
|
||||
case Right(pp: ProfilePointer) => Right(pp)
|
||||
case Right(evp: EventPointer) => Right(evp)
|
||||
case Right(sk: PrivateKey) => Right(sk)
|
||||
case Right(addr: AddressPointer) => Right(addr)
|
||||
case Left(_) if input.split(":").size == 3 =>
|
||||
// parse "a" tag format, nip 33
|
||||
val spl = input.split(":")
|
||||
(
|
||||
spl(0).toIntOption,
|
||||
ByteVector.fromHex(spl(1)),
|
||||
Some(spl(2))
|
||||
).mapN((kind, author, identifier) =>
|
||||
AddressPointer(
|
||||
identifier,
|
||||
kind,
|
||||
scoin.XOnlyPublicKey(ByteVector32(author)),
|
||||
relays = List.empty
|
||||
)
|
||||
).toRight("couldn't parse as a nip33 'a' tag")
|
||||
case Left(_) =>
|
||||
// parse event json
|
||||
parse(input) match {
|
||||
case Left(err: io.circe.ParsingFailure) =>
|
||||
Left("not valid JSON or NIP-19 code")
|
||||
case Right(json) =>
|
||||
json
|
||||
.as[Event]
|
||||
.leftMap { err =>
|
||||
err.pathToRootString match {
|
||||
case None => s"decoding ${err.pathToRootString}"
|
||||
case Some(path) => s"field $path is missing or wrong"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import cats.data.*
|
||||
import cats.effect.*
|
||||
import cats.effect.syntax.all.*
|
||||
import cats.syntax.all.*
|
||||
import fs2.concurrent.*
|
||||
import fs2.dom.{Event => _, *}
|
||||
import scoin.PrivateKey
|
||||
|
||||
case class Store(
|
||||
input: SignallingRef[IO, String],
|
||||
result: SignallingRef[IO, Result]
|
||||
)
|
||||
|
||||
object Store {
|
||||
def apply(window: Window[IO]): Resource[IO, Store] = {
|
||||
val key = "nak-input"
|
||||
|
||||
for {
|
||||
input <- SignallingRef[IO].of("").toResource
|
||||
result <- SignallingRef[IO, Result](Left("")).toResource
|
||||
|
||||
_ <- Resource.eval {
|
||||
OptionT(window.localStorage.getItem(key))
|
||||
.foreachF(input.set(_))
|
||||
}
|
||||
|
||||
_ <- window.localStorage
|
||||
.events(window)
|
||||
.foreach {
|
||||
case Storage.Event.Updated(`key`, _, value, _) =>
|
||||
input.set(value)
|
||||
case _ => IO.unit
|
||||
}
|
||||
.compile
|
||||
.drain
|
||||
.background
|
||||
|
||||
_ <- input.discrete
|
||||
.evalTap(input => IO.cede *> window.localStorage.setItem(key, input))
|
||||
.evalTap(input => result.set(Parser.parseInput(input.trim())))
|
||||
.compile
|
||||
.drain
|
||||
.background
|
||||
} yield Store(input, result)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import calico.html.io.*
|
||||
|
||||
object Styles {
|
||||
val button = cls :=
|
||||
"shrink bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 mx-2 px-4 rounded "
|
||||
val buttonSmall = cls :=
|
||||
"shrink text-sm bg-blue-500 hover:bg-blue-700 text-white font-bold mx-2 px-2 rounded "
|
||||
val mono = styleAttr := "font-family: monospace"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import io.circe.Printer
|
||||
import scodec.bits.ByteVector
|
||||
import scoin.*
|
||||
|
||||
object Utils {
|
||||
val keyOne = PrivateKey(ByteVector32(ByteVector(0x01).padLeft(32)))
|
||||
|
||||
val jsonPrinter = Printer(
|
||||
dropNullValues = false,
|
||||
indent = " ",
|
||||
lbraceRight = "\n",
|
||||
rbraceLeft = "\n",
|
||||
lbracketRight = "\n",
|
||||
rbracketLeft = "\n",
|
||||
lrbracketsEmpty = "",
|
||||
arrayCommaRight = "\n",
|
||||
objectCommaRight = "\n",
|
||||
colonLeft = "",
|
||||
colonRight = " ",
|
||||
sortKeys = true
|
||||
)
|
||||
}
|
||||
37
verify.go
Normal file
37
verify.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var verify = &cli.Command{
|
||||
Name: "verify",
|
||||
Usage: "checks the hash and signature of an event given through stdin",
|
||||
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.
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
evt := nostr.Event{}
|
||||
if stdinEvent := getStdin(); stdinEvent != "" {
|
||||
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
||||
return fmt.Errorf("invalid JSON: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if evt.GetID() != evt.ID {
|
||||
return fmt.Errorf("invalid .id, expected %s, got %s", evt.GetID(), evt.ID)
|
||||
}
|
||||
|
||||
if ok, err := evt.CheckSignature(); !ok {
|
||||
return fmt.Errorf("invalid signature: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user