Compare commits

..
10 Commits
33 changed files with 446 additions and 1392 deletions
+147
View File
@@ -0,0 +1,147 @@
{
"parserOptions": {
"ecmaVersion": 2020,
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module",
"allowImportExportEverywhere": false
},
"env": {
"es6": true,
"node": true
},
"plugins": [
"react",
"babel"
],
"globals": {
"document": false,
"navigator": false,
"window": false,
"location": false,
"URL": false,
"URLSearchParams": false,
"fetch": false,
"EventSource": false,
"localStorage": false,
"sessionStorage": false
},
"rules": {
"react/jsx-uses-vars": 2,
"react/jsx-no-undef": 2,
"react/jsx-uses-react": 2,
"accessor-pairs": 2,
"arrow-spacing": [2, { "before": true, "after": true }],
"block-spacing": [2, "always"],
"brace-style": [2, "1tbs", { "allowSingleLine": true }],
"comma-dangle": 0,
"comma-spacing": [2, { "before": false, "after": true }],
"comma-style": [2, "last"],
"constructor-super": 2,
"curly": [0, "multi-line"],
"dot-location": [2, "property"],
"eol-last": 2,
"eqeqeq": [2, "allow-null"],
"generator-star-spacing": [2, { "before": true, "after": true }],
"handle-callback-err": [2, "^(err|error)$" ],
"indent": 0,
"jsx-quotes": [2, "prefer-double"],
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
"keyword-spacing": [2, { "before": true, "after": true }],
"new-cap": 0,
"new-parens": 0,
"no-array-constructor": 2,
"no-caller": 2,
"no-class-assign": 2,
"no-cond-assign": 2,
"no-const-assign": 2,
"no-control-regex": 0,
"no-debugger": 0,
"no-delete-var": 2,
"no-dupe-args": 2,
"no-dupe-class-members": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-empty-character-class": 2,
"no-empty-pattern": 2,
"no-eval": 0,
"no-ex-assign": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-extra-boolean-cast": 2,
"no-extra-parens": [2, "functions"],
"no-fallthrough": 2,
"no-floating-decimal": 2,
"no-func-assign": 2,
"no-implied-eval": 2,
"no-inner-declarations": [0, "functions"],
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-iterator": 2,
"no-label-var": 2,
"no-labels": [2, { "allowLoop": false, "allowSwitch": false }],
"no-lone-blocks": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-multiple-empty-lines": [2, { "max": 2 }],
"no-native-reassign": 2,
"no-negated-in-lhs": 2,
"no-new": 0,
"no-new-func": 2,
"no-new-object": 2,
"no-new-require": 2,
"no-new-symbol": 2,
"no-new-wrappers": 2,
"no-obj-calls": 2,
"no-octal": 2,
"no-octal-escape": 2,
"no-path-concat": 0,
"no-proto": 2,
"no-redeclare": 2,
"no-regex-spaces": 2,
"no-return-assign": 0,
"no-self-assign": 2,
"no-self-compare": 2,
"no-sequences": 2,
"no-shadow-restricted-names": 2,
"no-spaced-func": 2,
"no-sparse-arrays": 2,
"no-this-before-super": 2,
"no-throw-literal": 2,
"no-trailing-spaces": 2,
"no-undef": 2,
"no-undef-init": 2,
"no-unexpected-multiline": 2,
"no-unneeded-ternary": [2, { "defaultAssignment": false }],
"no-unreachable": 2,
"no-unused-vars": [2, { "vars": "local", "args": "none", "varsIgnorePattern": "^_"}],
"no-useless-call": 2,
"no-useless-constructor": 2,
"no-with": 2,
"one-var": [0, { "initialized": "never" }],
"operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }],
"padded-blocks": [2, "never"],
"quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
"semi": [2, "never"],
"semi-spacing": [2, { "before": false, "after": true }],
"space-before-blocks": [2, "always"],
"space-before-function-paren": 0,
"space-in-parens": [2, "never"],
"space-infix-ops": 2,
"space-unary-ops": [2, { "words": true, "nonwords": false }],
"spaced-comment": 0,
"template-curly-spacing": [2, "never"],
"use-isnan": 2,
"valid-typeof": 2,
"wrap-iife": [2, "any"],
"yield-star-spacing": [2, "both"],
"yoda": 0,
"no-unsafe-optional-chaning": 0
}
}
-20
View File
@@ -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: .
+2 -8
View File
@@ -1,9 +1,3 @@
target
.bsp
globals.bundle.js
yarn.lock
node_modules
project/project
.metals
.bloop
project/metals.sbt
public/app.js
yarn.lock
+9
View File
@@ -0,0 +1,9 @@
arrowParens: avoid
bracketSpacing: false
jsxBracketSameLine: false
printWidth: 80
proseWrap: preserve
semi: false
singleQuote: true
trailingComma: none
useTabs: false
-2
View File
@@ -1,2 +0,0 @@
version = 3.5.8
runner.dialect = scala3
+1 -154
View File
@@ -1,154 +1 @@
# nostr army knife
this repository contains two things:
## a command-line tool for decoding and encoding nostr entities and talking to relays
Install with `go install github.com/fiatjaf/nak`.
It pairs nicely with https://github.com/blakejakopovic/nostcat using unix pipes.
### examples
```
~> 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 decode nevent1qqs29yet5tp0qq5xu5qgkeehkzqh5qu46739axzezcxpj4tjlkx9j7gpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5sh59ud
{
"id": "a2932ba2c2f00286e5008b6737b0817a0395d7a25e9859160c195572fd8c5979",
"relays": [
"wss://nostr-pub.wellorder.net"
]
}
~> nak req -a a2932ba2c2f00286e5008b6737b0817a0395d7a25e9859160c195572fd8c5979 -k 1 -a e8b487c079b0f67c695ae6c4c2552a47f38adfa2533cc5926bd2c102942fdcb7
["REQ","nak",{"kinds":[1],"authors":["a2932ba2c2f00286e5008b6737b0817a0395d7a25e9859160c195572fd8c5979","e8b487c079b0f67c695ae6c4c2552a47f38adfa2533cc5926bd2c102942fdcb7"]}]
```
### 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
event generates an encoded event
decode decodes nip19, nip21, nip05 or hex 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
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
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 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
```
written in go using [go-nostr](https://github.com/nbd-wtf/go-nostr), heavily inspired by [nostril](http://git.jb55.com/nostril/).
## a toolkit for debugging all things nostr as a webpage:
![](https://user-images.githubusercontent.com/1653275/227681805-0cd20b39-de0d-4fcb-abb4-de3283404e8f.png)
written in [scala](https://scala-lang.org/) with [calico](https://www.armanbilge.com/calico/) and [snow](https://github.com/fiatjaf/snow)
[![Netlify Status](https://api.netlify.com/api/v1/badges/2593eb9b-7f06-4e36-9448-4dc144245ba1/deploy-status)](https://app.netlify.com/sites/nostr-army-knife/deploys)
+59
View File
@@ -0,0 +1,59 @@
import React, {useState, useEffect} from 'react'
import {render} from 'react-dom'
import Nothing from './handlers/Nothing'
import EventSigning from './handlers/EventSigning'
import KeyHandling from './handlers/KeyHandling'
const handlers = [EventSigning, KeyHandling, Nothing]
function App() {
let [value, setValue] = useState(localStorage.getItem('value'))
useEffect(() => {
localStorage.setItem('value', value)
}, [value])
let Result
for (let i = 0; i < handlers.length; i++) {
let alt = handlers[i]
if (alt.match(value)) {
Result = alt
break
}
}
return (
<main style={{fontFamily: 'monospace'}}>
<div
style={{
width: '90%',
margin: 'auto'
}}
>
<h1>nostr army knife</h1>
paste something nostric
<textarea
value={value}
onChange={e => setValue(e.target.value)}
style={{
padding: '7px',
width: '100%',
minHeight: '200px'
}}
/>
</div>
<hr style={{margin: '18px 0'}} />
<div
style={{
width: '90%',
margin: 'auto'
}}
>
<Result value={value} />
</div>
</main>
)
}
render(<App />, document.getElementById('app'))
Executable
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env node
const esbuild = require('esbuild')
const alias = require('esbuild-plugin-alias')
const nodeGlobals = require('@esbuild-plugins/node-globals-polyfill').default
const prod = process.argv.indexOf('prod') !== -1
esbuild
.build({
bundle: true,
entryPoints: ['app.jsx'],
outdir: 'public',
plugins: [
alias({
stream: require.resolve('readable-stream')
}),
nodeGlobals({buffer: true})
],
sourcemap: prod ? false : 'inline',
define: {
window: 'self',
global: 'self'
}
})
.then(() => console.log('build success.'))
-14
View File
@@ -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) }
)
+19
View File
@@ -0,0 +1,19 @@
import React from 'react'
export default function Item({label, hint, children}) {
return (
<div
style={{
marginBottom: '9px',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
wordBreak: 'break-all'
}}
>
<b data-wenk={hint} data-wenk-pos="right">
{label}:{' '}
</b>
{children}
</div>
)
}
-109
View File
@@ -1,109 +0,0 @@
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"
"github.com/urfave/cli/v2"
)
var decode = &cli.Command{
Name: "decode",
Usage: "decodes nip19, nip21, nip05 or hex entities",
Description: `example usage:
nak decode npub1uescmd5krhrmj9rcura833xpke5eqzvcz5nxjw74ufeewf2sscxq4g7chm
nak decode nevent1qqs29yet5tp0qq5xu5qgkeehkzqh5qu46739axzezcxpj4tjlkx9j7gpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5sh59ud
nak decode nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpz4mhxue69uhk2er9dchxummnw3ezumrpdejqz8thwden5te0dehhxarj94c82c3wwajkcmr0wfjx2u3wdejhgqgcwaehxw309aex2mrp0yhxummnw3exzarf9e3k7mgnp0sh5
nak decode nsec1jrmyhtjhgd9yqalps8hf9mayvd58852gtz66m7tqpacjedkp6kxq4dyxsr`,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "id",
Aliases: []string{"e"},
Usage: "return just the event id, if applicable",
},
&cli.BoolFlag{
Name: "pubkey",
Aliases: []string{"p"},
Usage: "return just the pubkey, if applicable",
},
},
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()
if strings.HasPrefix(input, "nostr:") {
input = input[6:]
}
var decodeResult DecodeResult
if b, err := hex.DecodeString(input); err == nil {
if len(b) == 64 {
decodeResult.HexResult.PossibleTypes = []string{"sig"}
decodeResult.HexResult.Signature = hex.EncodeToString(b)
} else if len(b) == 32 {
decodeResult.HexResult.PossibleTypes = []string{"pubkey", "private_key", "event_id"}
decodeResult.HexResult.ID = hex.EncodeToString(b)
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))
}
} else if evp := sdk.InputToEventPointer(input); evp != nil {
decodeResult = DecodeResult{EventPointer: evp}
} else if pp := sdk.InputToProfile(c.Context, input); pp != nil {
decodeResult = DecodeResult{ProfilePointer: pp}
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" {
ep := value.(nostr.EntityPointer)
decodeResult = DecodeResult{EntityPointer: &ep}
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" {
decodeResult.PrivateKey.PrivateKey = value.(string)
decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string))
} else {
return fmt.Errorf("couldn't decode input")
}
fmt.Println(decodeResult.JSON())
return nil
},
}
type DecodeResult struct {
*nostr.EventPointer
*nostr.ProfilePointer
*nostr.EntityPointer
HexResult struct {
PossibleTypes []string `json:"possible_types"`
PublicKey string `json:"pubkey,omitempty"`
ID string `json:"event_id,omitempty"`
PrivateKey string `json:"private_key,omitempty"`
Signature string `json:"sig,omitempty"`
}
PrivateKey struct {
nostr.ProfilePointer
PrivateKey string `json:"private_key"`
}
}
func (d DecodeResult) JSON() string {
var j []byte
if d.EventPointer != nil {
j, _ = json.MarshalIndent(d.EventPointer, "", " ")
} else if d.ProfilePointer != nil {
j, _ = json.MarshalIndent(d.ProfilePointer, "", " ")
} else if d.EntityPointer != nil {
j, _ = json.MarshalIndent(d.EntityPointer, "", " ")
} else if len(d.HexResult.PossibleTypes) > 0 {
j, _ = json.MarshalIndent(d.HexResult, "", " ")
} else if d.PrivateKey.PrivateKey != "" {
j, _ = json.MarshalIndent(d.PrivateKey, "", " ")
}
return string(j)
}
-125
View File
@@ -1,125 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/nbd-wtf/go-nostr"
"github.com/urfave/cli/v2"
)
const CATEGORY_EVENT_FIELDS = "EVENT FIELDS"
var event = &cli.Command{
Name: "event",
Usage: "generates an encoded event",
Description: `example usage (for sending directly to a relay with 'nostcat'):
nak event -k 1 -c hello --envelope | nostcat wss://nos.lol`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "sec",
Usage: "secret key to sign the event",
DefaultText: "the key '1'",
Value: "0000000000000000000000000000000000000000000000000000000000000001",
},
&cli.BoolFlag{
Name: "envelope",
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
},
&cli.IntFlag{
Name: "kind",
Aliases: []string{"k"},
Usage: "event kind",
DefaultText: "1",
Value: 1,
Category: CATEGORY_EVENT_FIELDS,
},
&cli.StringFlag{
Name: "content",
Aliases: []string{"c"},
Usage: "event content",
DefaultText: "hello from the nostr army knife",
Value: "hello from the nostr army knife",
Category: CATEGORY_EVENT_FIELDS,
},
&cli.StringSliceFlag{
Name: "tag",
Aliases: []string{"t"},
Usage: "sets a tag field on the event, takes a value like -t e=<id>",
Category: CATEGORY_EVENT_FIELDS,
},
&cli.StringSliceFlag{
Name: "e",
Usage: "shortcut for --tag e=<value>",
Category: CATEGORY_EVENT_FIELDS,
},
&cli.StringSliceFlag{
Name: "p",
Usage: "shortcut for --tag p=<value>",
Category: CATEGORY_EVENT_FIELDS,
},
&cli.StringFlag{
Name: "created-at",
Aliases: []string{"time", "ts"},
Usage: "unix timestamp value for the created_at field",
DefaultText: "now",
Value: "now",
Category: CATEGORY_EVENT_FIELDS,
},
},
Action: func(c *cli.Context) error {
evt := nostr.Event{
Kind: c.Int("kind"),
Content: c.String("content"),
Tags: make(nostr.Tags, 0, 3),
}
tags := make([][]string, 0, 5)
for _, tagFlag := range c.StringSlice("tag") {
spl := strings.Split(tagFlag, "=")
if len(spl) == 2 && len(spl[0]) == 1 {
tags = append(tags, spl)
}
}
for _, etag := range c.StringSlice("e") {
tags = append(tags, []string{"e", etag})
}
for _, ptag := range c.StringSlice("p") {
tags = append(tags, []string{"p", ptag})
}
if len(tags) > 0 {
for _, tag := range tags {
evt.Tags = append(evt.Tags, tag)
}
}
createdAt := c.String("created-at")
ts := time.Now()
if createdAt != "now" {
if v, err := strconv.ParseInt(createdAt, 10, 64); err != nil {
return fmt.Errorf("failed to parse timestamp '%s': %w", createdAt, err)
} else {
ts = time.Unix(v, 0)
}
}
evt.CreatedAt = nostr.Timestamp(ts.Unix())
if err := evt.Sign(c.String("sec")); err != nil {
return fmt.Errorf("error signing with provided key: %w", err)
}
var result string
if c.Bool("envelope") {
j, _ := json.Marshal([]any{"EVENT", evt})
result = string(j)
} else {
result = evt.String()
}
fmt.Println(result)
return nil
},
}
-1
View File
@@ -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="#666" stroke-width="1.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path></svg>

Before

Width:  |  Height:  |  Size: 278 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

-30
View File
@@ -1,30 +0,0 @@
module github.com/fiatjaf/nak
go 1.20
require (
github.com/nbd-wtf/go-nostr v0.18.3
github.com/urfave/cli/v2 v2.25.3
)
require (
github.com/SaveTheRbtz/generic-sync-map-go v0.0.0-20220414055132-a37292614db8 // indirect
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/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/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.2.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // 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
)
-134
View File
@@ -1,134 +0,0 @@
github.com/SaveTheRbtz/generic-sync-map-go v0.0.0-20220414055132-a37292614db8 h1:Xa6tp8DPDhdV+k23uiTC/GrAYOe4IdyJVKtob4KW3GA=
github.com/SaveTheRbtz/generic-sync-map-go v0.0.0-20220414055132-a37292614db8/go.mod h1:ihkm1viTbO/LOsgdGoFPBSvzqvx7ibvkMzYp3CgtHik=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.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/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/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=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
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/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
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/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=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
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.18.3 h1:ofMYxlFAptyoErlOGOCUk7zGHQNJ8/ZkIXXOsveFZ+c=
github.com/nbd-wtf/go-nostr v0.18.3/go.mod h1:GPJOOK8US38kz+bfb9nWe873Xu0e6bXlThejOs1LTkc=
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=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/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/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/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/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=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/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=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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.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=
+103
View File
@@ -0,0 +1,103 @@
import React, {useState} from 'react'
import useComputedState from 'use-computed-state'
import {
getPublicKey,
getEventHash,
serializeEvent,
verifySignature,
signEvent
} from 'nostr-tools'
import Item from '../components/item'
export default function EventSigning({value}) {
let evt = JSON.parse(value)
let [privateKey, setPrivateKey] = useState(
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
)
let privateKeyIsValid = useComputedState(
() => privateKey.match(/^[a-f0-9]{64}$/),
[privateKey]
)
let publicKey = useComputedState(
() => (privateKeyIsValid ? getPublicKey(privateKey) : null),
[privateKeyIsValid]
)
let signature = useComputedState(async () => {
if (evt.sig) return null
try {
evt.pubkey = publicKey
return await signEvent(evt, privateKey)
} catch (err) {
return null
}
}, [value, privateKey])
let eventHash = useComputedState(() => getEventHash(evt), [value])
let isValidHash = useComputedState(
() => evt.id === eventHash,
[value, eventHash]
)
let isValidSignature = useComputedState(async () => {
if (evt.id && evt.sig) {
try {
return await verifySignature(evt)
} catch (err) {
return false
}
} else {
return null
}
}, [value])
return (
<>
<Item
label="serialized event"
hint="according to nip-01 signature algorithm"
>
{serializeEvent(evt)}
</Item>
<Item label="event id" hint="sha256 hash of serialized event">
{eventHash}
</Item>
<Item
label="event id matches hash"
hint="the calculated hash from event data matches the 'id' in the body?"
>
{isValidHash?.toString()}
</Item>
{isValidHash &&
(evt.sig ? (
<Item label="signature valid">
{isValidSignature?.toString() || ''}
</Item>
) : (
<>
<Item
label="private key"
hint="paste any private key here (32 bytes hex-encoded)"
>
<input
value={privateKey}
onChange={e => setPrivateKey(e.target.value.toLowerCase())}
/>{' '}
{privateKeyIsValid ? 'valid' : 'invalid'}
</Item>
<Item label="public key">{publicKey}</Item>
<Item label="signature">{privateKeyIsValid ? signature : ''}</Item>
</>
))}
</>
)
}
EventSigning.match = value => {
try {
let evt = JSON.parse(value)
return evt.kind && evt.content && evt.tags
} catch (err) {
/**/
}
return false
}
+27
View File
@@ -0,0 +1,27 @@
import React from 'react'
import useBooleanState from 'use-boolean-state'
import {getPublicKey} from 'nostr-tools'
import Item from '../components/item'
export default function KeyHandling({value}) {
let privateKey = value
let publicKey = getPublicKey(privateKey)
return (
<>
<Item label="private key">{privateKey}</Item>
<Item label="public key">{publicKey}</Item>
</>
)
}
KeyHandling.match = value => {
try {
if (value.toLowerCase().match(/^[a-f0-9]{64}$/)) return true
} catch (err) {
/**/
}
return false
}
+28
View File
@@ -0,0 +1,28 @@
import React from 'react'
export default function Nothing() {
return (
<>
you can paste
<ul>
<li>an unsigned event to be hashed and signed</li>
<li>a signed event to have its signature checked</li>
<li>a nostr relay URL to be inspected</li>
<li>a nostr event id we'll try to fetch</li>
<li>a nip05 identifier to be checked</li>
<li>
contribute a new function:{' '}
<a
target="_blank"
style={{color: 'inherit'}}
href="https://github.com/fiatjaf/nostr-army-knife"
>
_______
</a>
</li>
</ul>
</>
)
}
Nothing.match = () => true
-7
View File
@@ -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
View File
@@ -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
-25
View File
@@ -1,25 +0,0 @@
package main
import (
"fmt"
"os"
"github.com/urfave/cli/v2"
)
func main() {
app := &cli.App{
Name: "nak",
Usage: "the nostr army knife command-line tool",
Commands: []*cli.Command{
req,
event,
decode,
},
}
if err := app.Run(os.Args); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"dependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
"esbuild": "^0.14.21",
"esbuild-plugin-alias": "^0.2.1",
"events": "^3.3.0",
"nostr-tools": "^0.22.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"use-boolean-state": "^1.0.2",
"use-computed-state": "1.1.0"
},
"scripts": {
"build": "./build.js prod",
"watch": "ag -l --js | entr ./build.js"
}
}
-1
View File
@@ -1 +0,0 @@
sbt.version=1.7.1
-2
View File
@@ -1,2 +0,0 @@
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.0")
addSbtPlugin("com.fiatjaf" % "sbt-esbuild" % "0.1.1")
+8
View File
@@ -0,0 +1,8 @@
<meta charset="utf-8" />
<title>nostr army knife</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/wenk/1.0.8/wenk.min.css"
/>
<div id="app" />
<script src="app.js"></script>
-163
View File
@@ -1,163 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"strings"
"github.com/nbd-wtf/go-nostr"
"github.com/urfave/cli/v2"
)
const CATEGORY_FILTER_ATTRIBUTES = "FILTER ATTRIBUTES"
var req = &cli.Command{
Name: "req",
Usage: "generates encoded REQ messages and optionally use them to talk to relays",
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`,
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "author",
Aliases: []string{"a"},
Usage: "only accept events from these authors (pubkey as hex)",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.StringSliceFlag{
Name: "id",
Aliases: []string{"i"},
Usage: "only accept events with these ids (hex)",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.IntSliceFlag{
Name: "kind",
Aliases: []string{"k"},
Usage: "only accept events with these kind numbers",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.StringSliceFlag{
Name: "tag",
Aliases: []string{"t"},
Usage: "takes a tag like -t e=<id>, only accept events with these tags",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.StringSliceFlag{
Name: "e",
Usage: "shortcut for --tag e=<value>",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.StringSliceFlag{
Name: "p",
Usage: "shortcut for --tag p=<value>",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.IntFlag{
Name: "since",
Aliases: []string{"s"},
Usage: "only accept events newer than this (unix timestamp)",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.IntFlag{
Name: "until",
Aliases: []string{"u"},
Usage: "only accept events older than this (unix timestamp)",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.IntFlag{
Name: "limit",
Aliases: []string{"l"},
Usage: "only accept up to this number of events",
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",
},
},
ArgsUsage: "[relay...]",
Action: func(c *cli.Context) error {
filter := nostr.Filter{}
if authors := c.StringSlice("author"); len(authors) > 0 {
filter.Authors = authors
}
if ids := c.StringSlice("id"); len(ids) > 0 {
filter.IDs = ids
}
if kinds := c.IntSlice("kind"); len(kinds) > 0 {
filter.Kinds = kinds
}
tags := make([][]string, 0, 5)
for _, tagFlag := range c.StringSlice("tag") {
spl := strings.Split(tagFlag, "=")
if len(spl) == 2 && len(spl[0]) == 1 {
tags = append(tags, spl)
} else {
return fmt.Errorf("invalid --tag '%s'", tagFlag)
}
}
for _, etag := range c.StringSlice("e") {
tags = append(tags, []string{"e", etag})
}
for _, ptag := range c.StringSlice("p") {
tags = append(tags, []string{"p", ptag})
}
if len(tags) > 0 {
filter.Tags = make(nostr.TagMap)
for _, tag := range tags {
if _, ok := filter.Tags[tag[0]]; !ok {
filter.Tags[tag[0]] = make([]string, 0, 3)
}
filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1])
}
}
if since := c.Int("since"); since != 0 {
ts := nostr.Timestamp(since)
filter.Since = &ts
}
if until := c.Int("until"); until != 0 {
ts := nostr.Timestamp(until)
filter.Until = &ts
}
if limit := c.Int("limit"); limit != 0 {
filter.Limit = limit
}
relays := c.Args().Slice()
if len(relays) > 0 {
pool := nostr.NewSimplePool(c.Context)
fn := pool.SubManyEose
if c.Bool("stream") {
fn = pool.SubMany
}
for evt := range fn(c.Context, relays, nostr.Filters{filter}) {
fmt.Println(evt)
}
} else {
// no relays given, will just print the filter
var result string
if c.Bool("bare") {
result = filter.String()
} else {
j, _ := json.Marshal([]any{"REQ", "nak", filter})
result = string(j)
}
fmt.Println(result)
}
return nil
},
}
-315
View File
@@ -1,315 +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(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))
),
nip19_21(
"nprofile",
NIP19.encode(ProfilePointer(XOnlyPublicKey(bytes32)))
)
),
"if this is a private key:",
div(
cls := "pl-2 mb-2",
entry(
"nsec",
NIP19.encode(PrivateKey(bytes32))
),
entry(
"npub",
NIP19.encode(XOnlyPublicKey(bytes32))
),
nip19_21(
"nprofile",
NIP19.encode(ProfilePointer(XOnlyPublicKey(bytes32)))
)
),
"if this is an event id:",
div(
cls := "pl-2 mb-2",
nip19_21(
"nevent",
NIP19.encode(EventPointer(bytes32.toHex))
)
),
div(
cls := "pl-2 mb-2",
entry(
"note",
NIP19.encode(bytes32)
)
)
)
def renderEventPointer(
store: Store,
evp: snow.EventPointer
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
entry("event id (hex)", evp.id),
relayHints(store, evp.relays),
evp.author.map { pk =>
entry("author hint (pubkey hex)", pk.value.toHex)
},
nip19_21("nevent", NIP19.encode(evp)),
entry("note", 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) },
sk.map { k => entry("nsec", NIP19.encode(k)) },
entry("public key (hex)", pp.pubkey.value.toHex),
relayHints(
store,
pp.relays,
dynamic = if sk.isDefined then false else true
),
entry("npub", NIP19.encode(pp.pubkey)),
nip19_21("nprofile", NIP19.encode(pp))
)
def renderAddressPointer(
store: Store,
addr: snow.AddressPointer
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
entry("author (pubkey hex)", addr.author.value.toHex),
entry("identifier", addr.d),
entry("kind", addr.kind.toString),
relayHints(store, addr.relays),
nip19_21("naddr", NIP19.encode(addr))
)
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(
"nevent",
NIP19.encode(EventPointer(id, author = event.pubkey))
)
),
event.id.map(id =>
entry(
"note",
NIP19.encode(ByteVector32.fromValidHex(id))
)
)
)
private def entry(
key: String,
value: String
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "flex items-center space-x-3",
span(cls := "font-bold", key + " "),
span(Styles.mono, cls := "max-w-xl", value)
)
private def nip19_21(
key: String,
code: String
): Resource[IO, HtmlDivElement[IO]] =
div(
span(cls := "font-bold", key + " "),
span(Styles.mono, cls := "break-all", 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 "),
span(Styles.mono, cls := "max-w-xl", value),
active.map {
case true =>
div(
input.withSelf { self =>
(
onKeyPress --> (_.foreach(evt =>
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 = url :: a.relays)
)
case p: ProfilePointer =>
NIP19
.encode(
p.copy(relays = url :: p.relays)
)
case e: EventPointer =>
NIP19
.encode(
e.copy(relays = url :: e.relays)
)
case r => ""
}
.getOrElse("")
)
)
>> active.set(false)
} else IO.unit
)
case _ => IO.unit
}
))
)
}
)
case false if dynamic =>
button(
Styles.buttonSmall,
"add relay hint",
onClick --> (_.foreach(_ => active.set(true)))
)
case false => div("")
}
)
}
private val external = img(cls := "inline w-4 ml-2", src := "ext.svg")
}
-147
View File
@@ -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 h-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(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)
}
)
}
-45
View File
@@ -1,45 +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(_) =>
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"
}
}
}
}
)
}
-46
View File
@@ -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)
}
}
-9
View File
@@ -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"
}
-22
View File
@@ -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
)
}