diff --git a/.github/workflows/publish-webapp.yml b/.github/workflows/publish-webapp.yml
deleted file mode 100644
index eb2d043..0000000
--- a/.github/workflows/publish-webapp.yml
+++ /dev/null
@@ -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: .
diff --git a/build.sbt b/build.sbt
deleted file mode 100644
index 36ebb0b..0000000
--- a/build.sbt
+++ /dev/null
@@ -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) }
- )
diff --git a/edit.svg b/edit.svg
deleted file mode 100644
index abfcd8b..0000000
--- a/edit.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
diff --git a/ext.svg b/ext.svg
deleted file mode 100644
index 09048c0..0000000
--- a/ext.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/favicon.ico b/favicon.ico
deleted file mode 100644
index 3736df3..0000000
Binary files a/favicon.ico and /dev/null differ
diff --git a/index.html b/index.html
deleted file mode 100644
index faebfe1..0000000
--- a/index.html
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
nostr army knife
-
-
-
-
-
diff --git a/justfile b/justfile
deleted file mode 100644
index 50881fb..0000000
--- a/justfile
+++ /dev/null
@@ -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
diff --git a/project/build.properties b/project/build.properties
deleted file mode 100644
index 22af262..0000000
--- a/project/build.properties
+++ /dev/null
@@ -1 +0,0 @@
-sbt.version=1.7.1
diff --git a/project/plugins.sbt b/project/plugins.sbt
deleted file mode 100644
index dabefb2..0000000
--- a/project/plugins.sbt
+++ /dev/null
@@ -1,2 +0,0 @@
-addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.0")
-addSbtPlugin("com.fiatjaf" % "sbt-esbuild" % "0.1.1")
diff --git a/src/main/scala/Components.scala b/src/main/scala/Components.scala
deleted file mode 100644
index 6137b14..0000000
--- a/src/main/scala/Components.scala
+++ /dev/null
@@ -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")
-}
diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala
deleted file mode 100644
index 7f3c914..0000000
--- a/src/main/scala/Main.scala
+++ /dev/null
@@ -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)
- }
- )
-}
diff --git a/src/main/scala/Parser.scala b/src/main/scala/Parser.scala
deleted file mode 100644
index 9033aaa..0000000
--- a/src/main/scala/Parser.scala
+++ /dev/null
@@ -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"
- }
- }
- }
- }
- )
-}
diff --git a/src/main/scala/Store.scala b/src/main/scala/Store.scala
deleted file mode 100644
index 35dadd8..0000000
--- a/src/main/scala/Store.scala
+++ /dev/null
@@ -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)
- }
-}
diff --git a/src/main/scala/Styles.scala b/src/main/scala/Styles.scala
deleted file mode 100644
index 4a72181..0000000
--- a/src/main/scala/Styles.scala
+++ /dev/null
@@ -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"
-}
diff --git a/src/main/scala/Utils.scala b/src/main/scala/Utils.scala
deleted file mode 100644
index 7a64810..0000000
--- a/src/main/scala/Utils.scala
+++ /dev/null
@@ -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
- )
-}