nip19 parsing.

This commit is contained in:
fiatjaf 2023-03-24 20:46:59 -03:00
parent b966c8a1ac
commit d54b776c38
No known key found for this signature in database
GPG Key ID: BAD43C4BE5C1A3A1
3 changed files with 132 additions and 40 deletions

View File

@ -0,0 +1,86 @@
import cats.data.*
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.*
object Components {
def renderEventPointer(
evp: snow.EventPointer
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
entry("event id (hex)", evp.id),
if evp.relays.size > 0 then
Some(entry("relay hints", evp.relays.reduce((a, b) => s"$a, $b")))
else None,
evp.author.map { pk =>
entry("author hint (pubkey hex)", pk.value.toHex)
}
)
def renderProfilePointer(
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) },
entry("public key (hex)", pp.pubkey.value.toHex),
if pp.relays.size > 0 then
Some(entry("relay hints", pp.relays.reduce((a, b) => s"$a, $b")))
else None
)
def renderAddressPointer(
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),
if addr.relays.size > 0 then
Some(entry("relay hints", addr.relays.reduce((a, b) => s"$a, $b")))
else None
)
def renderEvent(event: Event): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
List(("pubkey", event.pubkey), ("id", event.id), ("sig", event.sig))
.filter((_, v) => v.isEmpty)
.map { (label, _) => entry("property missing", label) },
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"
}
)
)
private def entry(
key: String,
value: String
): Resource[IO, HtmlDivElement[IO]] =
div(
span(cls := "font-bold", key + " "),
span(Styles.mono, value)
)
}

View File

@ -13,6 +13,7 @@ import scoin.*
import snow.* import snow.*
import Utils.* import Utils.*
import Components.*
object Store { object Store {
def apply(window: Window[IO]): Resource[IO, Store] = { def apply(window: Window[IO]): Resource[IO, Store] = {
@ -105,6 +106,7 @@ object Main extends IOWebApp {
( (
cls := "w-full max-h-96 p-3 rounded", cls := "w-full max-h-96 p-3 rounded",
styleAttr := "min-height: 280px; font-family: monospace", styleAttr := "min-height: 280px; font-family: monospace",
spellCheck := false,
placeholder := "paste something nostric", placeholder := "paste something nostric",
onInput --> (_.foreach(_ => onInput --> (_.foreach(_ =>
self.value.get.flatMap(store.input.set) self.value.get.flatMap(store.input.set)
@ -121,47 +123,18 @@ object Main extends IOWebApp {
store.input.map { input => store.input.map { input =>
if input.trim() == "" then div("") if input.trim() == "" then div("")
else else
decode[Event](input) match { Parser.parseInput(input) match {
case Left(err: io.circe.ParsingFailure) => case Left(msg) => div(msg)
div("not valid JSON") case Right(event: Event) =>
case Left(err: io.circe.DecodingFailure) => renderEvent(event)
err.pathToRootString match { case Right(pp: ProfilePointer) => renderProfilePointer(pp)
case None => div(s"decoding ${err.pathToRootString}") case Right(evp: EventPointer) => renderEventPointer(evp)
case Some(path) => div(s"field $path is missing or wrong") case Right(sk: PrivateKey) =>
} renderProfilePointer(
case Right(event) => ProfilePointer(pubkey = sk.publicKey.xonly),
div( Some(sk)
cls := "text-md",
div(
span(cls := "font-bold", "serialized event "),
span(Styles.mono, event.serialized)
),
div(
span(cls := "font-bold", "implied event id "),
span(Styles.mono, event.hash.toHex)
),
div(
span(
cls := "font-bold",
"does the implied event id match the given event id? "
),
span(
Styles.mono,
event.id == event.hash.toHex match {
case true => "yes"; case false => "no"
}
)
),
div(
span(cls := "font-bold", "is signature valid? "),
span(
Styles.mono,
event.isValid match {
case true => "yes"; case false => "no"
}
)
)
) )
case Right(addr: AddressPointer) => renderAddressPointer(addr)
} }
} }

View File

@ -0,0 +1,33 @@
import io.circe.parser.*
import cats.syntax.all.*
import scoin.*
import snow.*
import Components.*
object Parser {
def parseInput(input: String): Either[
String,
Event | PrivateKey | AddressPointer | EventPointer | ProfilePointer
] =
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"
}
}
}
}
}