Merge branch 'main' of https://github.com/vitorpamplona/amethyst into upstream-main

# Conflicts:
#	gradle/libs.versions.toml
#	quartz/build.gradle.kts
#	quartz/src/androidHostTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.android.kt
#	quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/GZip.ios.kt
#	quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.kt
This commit is contained in:
KotlinGeekDev
2026-03-04 00:37:10 +01:00
581 changed files with 36083 additions and 4749 deletions
@@ -56,18 +56,20 @@ class HintIndexerTest {
)
}
val relays =
TestResourceLoader()
.loadString("relayDB.txt")
.split('\n')
.mapNotNull {
val relay = RelayUrlNormalizer.normalizeOrNull(it)
if (relay == null || relay.isLocalHost()) {
null
} else {
relay
val relays by
lazy {
TestResourceLoader()
.loadString("relayDB.txt")
.split('\n')
.mapNotNull {
val relay = RelayUrlNormalizer.normalizeOrNull(it)
if (relay == null || relay.isLocalHost()) {
null
} else {
relay
}
}
}
}
val indexer by lazy {
val result = HintIndexer()
@@ -0,0 +1,288 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip64Chess
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* Tests for ChessGameEvent (NIP-64 Kind 64 event)
*
* Verifies:
* - Event kind is 64
* - PGN content storage
* - Alt text support (NIP-31)
* - Event structure compliance
*/
class ChessGameEventTest {
private val samplePGN =
"""
[Event "Test Game"]
[Site "Internet"]
[Date "2024.12.28"]
[Round "1"]
[White "Alice"]
[Black "Bob"]
[Result "1-0"]
1. e4 e5 2. Qh5 Nc6 3. Bc4 Nf6 4. Qxf7# 1-0
""".trimIndent()
@Test
fun `verify event kind is 64`() {
assertEquals(64, ChessGameEvent.KIND, "Chess game event should be kind 64")
}
@Test
fun `pgn content is accessible`() {
// Create a mock event manually for testing
val testEvent =
ChessGameEvent(
id = "test_id",
pubKey = "test_pubkey",
createdAt = 1000L,
tags = arrayOf(arrayOf("alt", "Chess Game")),
content = samplePGN,
sig = "test_sig",
)
assertEquals(samplePGN, testEvent.pgn(), "PGN content should be accessible via pgn()")
assertEquals(samplePGN, testEvent.content, "PGN should be in content field")
}
@Test
fun `alt text is accessible when present`() {
val customAltText = "Scholar's Mate Example"
val testEvent =
ChessGameEvent(
id = "test_id",
pubKey = "test_pubkey",
createdAt = 1000L,
tags = arrayOf(arrayOf("alt", customAltText)),
content = samplePGN,
sig = "test_sig",
)
assertEquals(customAltText, testEvent.altText(), "Alt text should be extractable from tags")
}
@Test
fun `alt text returns null when not present`() {
val testEvent =
ChessGameEvent(
id = "test_id",
pubKey = "test_pubkey",
createdAt = 1000L,
tags = emptyArray(),
content = samplePGN,
sig = "test_sig",
)
assertEquals(null, testEvent.altText(), "Should return null when no alt tag present")
}
@Test
fun `event can store complete game with metadata`() {
val testEvent =
ChessGameEvent(
id = "test_id",
pubKey = "test_pubkey",
createdAt = 1000L,
tags = arrayOf(arrayOf("alt", "Chess Game")),
content = samplePGN,
sig = "test_sig",
)
// Parse the PGN to verify it's valid
val gameResult = PGNParser.parse(testEvent.pgn())
assertTrue(gameResult.isSuccess, "Event should contain valid PGN")
val game = gameResult.getOrThrow()
assertEquals("Test Game", game.event)
assertEquals("Alice", game.white)
assertEquals("Bob", game.black)
assertEquals(GameResult.WHITE_WINS, game.result)
}
@Test
fun `event can store minimal PGN`() {
val minimalPGN = "1. e4 *"
val testEvent =
ChessGameEvent(
id = "test_id",
pubKey = "test_pubkey",
createdAt = 1000L,
tags = arrayOf(arrayOf("alt", "Chess Game")),
content = minimalPGN,
sig = "test_sig",
)
val gameResult = PGNParser.parse(testEvent.pgn())
assertTrue(gameResult.isSuccess, "Should handle minimal PGN")
val game = gameResult.getOrThrow()
assertEquals(1, game.moves.size)
assertEquals(GameResult.IN_PROGRESS, game.result)
}
@Test
fun `event can store game in progress`() {
val inProgressPGN =
"""
[Event "Live Game"]
[Result "*"]
1. e4 e5 2. Nf3 Nc6 3. Bb5 *
""".trimIndent()
val testEvent =
ChessGameEvent(
id = "test_id",
pubKey = "test_pubkey",
createdAt = 1000L,
tags = arrayOf(arrayOf("alt", "Live Chess Game")),
content = inProgressPGN,
sig = "test_sig",
)
val gameResult = PGNParser.parse(testEvent.pgn())
assertTrue(gameResult.isSuccess)
val game = gameResult.getOrThrow()
assertEquals(GameResult.IN_PROGRESS, game.result)
assertEquals("*", game.metadata["Result"])
}
@Test
fun `event preserves PGN formatting`() {
val formattedPGN =
"""
[Event "Formatted Game"]
[White "Player 1"]
[Black "Player 2"]
1. e4 e5
2. Nf3 Nc6
3. Bb5 a6
*
""".trimIndent()
val testEvent =
ChessGameEvent(
id = "test_id",
pubKey = "test_pubkey",
createdAt = 1000L,
tags = arrayOf(arrayOf("alt", "Chess Game")),
content = formattedPGN,
sig = "test_sig",
)
// Content should be preserved exactly as provided
assertEquals(formattedPGN, testEvent.pgn())
}
@Test
fun `default alt text is Chess Game`() {
assertEquals("Chess Game", ChessGameEvent.ALT_DESCRIPTION)
}
@Test
fun `event inherits from Event base class`() {
val testEvent =
ChessGameEvent(
id = "test_id",
pubKey = "test_pubkey",
createdAt = 1000L,
tags = emptyArray(),
content = "1. e4 *",
sig = "test_sig",
)
// Verify base Event properties
assertEquals("test_id", testEvent.id)
assertEquals("test_pubkey", testEvent.pubKey)
assertEquals(1000L, testEvent.createdAt)
assertEquals(64, testEvent.kind)
assertEquals("test_sig", testEvent.sig)
}
@Test
fun `event can contain long tournament game`() {
val longPGN =
"""
[Event "Tournament Game"]
[Site "Online"]
[Date "2024.12.28"]
[Round "5"]
[White "GM Player"]
[Black "IM Player"]
[Result "1/2-1/2"]
1. d4 Nf6 2. c4 g6 3. Nc3 Bg7 4. e4 d6 5. Nf3 O-O 6. Be2 e5
7. O-O Nc6 8. d5 Ne7 9. Ne1 Nd7 10. Nd3 f5 11. Bd2 Nf6 12. f3 f4
13. Rc1 g5 14. Nb5 Ng6 15. c5 Rf7 16. Qa4 h5 17. Rfe1 Bf8
18. cxd6 cxd6 19. Rc6 Bd7 20. Rec1 Bxc6 21. Rxc6 Qd7 22. Rc1 Rc8
23. Rxc8 Qxc8 24. Qa6 Qc2 25. Qxb7 Qxb2 26. Qxa7 Qxa2 27. Qb7 Qa1+
28. Kf2 Qb2 29. Qa7 Ra7 30. Qa4 Qa2 31. Qa8 Qb2 32. Qa4 Qa2 1/2-1/2
""".trimIndent()
val testEvent =
ChessGameEvent(
id = "test_id",
pubKey = "test_pubkey",
createdAt = 1000L,
tags = arrayOf(arrayOf("alt", "Long Tournament Game")),
content = longPGN,
sig = "test_sig",
)
val gameResult = PGNParser.parse(testEvent.pgn())
assertTrue(gameResult.isSuccess)
val game = gameResult.getOrThrow()
assertTrue(game.moves.size > 50, "Should handle long games")
assertEquals(GameResult.DRAW, game.result)
}
@Test
fun `verify tags array structure`() {
val testEvent =
ChessGameEvent(
id = "test_id",
pubKey = "test_pubkey",
createdAt = 1000L,
tags =
arrayOf(
arrayOf("alt", "Test Alt Text"),
arrayOf("t", "chess"),
arrayOf("t", "game"),
),
content = "1. e4 *",
sig = "test_sig",
)
// Verify tags structure
assertTrue(testEvent.tags.size >= 1)
assertEquals("alt", testEvent.tags[0][0])
assertEquals("Test Alt Text", testEvent.tags[0][1])
}
}
@@ -0,0 +1,573 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip64Chess
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
/**
* Tests for JesterEvent (Jester Protocol kind 30 events)
*
* Verifies:
* - Event kind is 30
* - JSON content parsing (version, kind, fen, move, history)
* - e-tag structure for event linking
* - p-tag for opponent tagging
* - Start vs Move event detection
* - Compatibility with jesterui protocol
*
* Reference: https://github.com/jesterui/jesterui/blob/devel/FLOW.md
*/
class JesterEventTest {
private val testPubkey = "abc123def456"
private val opponentPubkey = "opponent789xyz"
private val startEventId = "start-event-id-001"
// ==========================================================================
// PROTOCOL CONSTANTS
// ==========================================================================
@Test
fun `verify event kind is 30`() {
assertEquals(30, JesterProtocol.KIND, "Jester protocol uses kind 30")
assertEquals(30, JesterEvent.KIND, "JesterEvent.KIND should be 30")
}
@Test
fun `verify start position hash constant`() {
assertEquals(
"b1791d7fc9ae3d38966568c257ffb3a02cbf8394cdb4805bc70f64fc3c0b6879",
JesterProtocol.START_POSITION_HASH,
"Should match jesterui's START_POSITION_HASH",
)
}
@Test
fun `verify starting FEN constant`() {
assertEquals(
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
JesterProtocol.FEN_START,
"Standard chess starting position",
)
}
@Test
fun `verify content kind constants`() {
assertEquals(0, JesterProtocol.CONTENT_KIND_START, "Start event kind should be 0")
assertEquals(1, JesterProtocol.CONTENT_KIND_MOVE, "Move event kind should be 1")
assertEquals(2, JesterProtocol.CONTENT_KIND_CHAT, "Chat event kind should be 2")
}
// ==========================================================================
// START EVENT TESTS
// ==========================================================================
@Test
fun `parse start event - open challenge`() {
val content = """{"version":"0","kind":0,"fen":"${JesterProtocol.FEN_START}","history":[],"nonce":"abc12345","playerColor":"white"}"""
val event =
JesterEvent(
id = startEventId,
pubKey = testPubkey,
createdAt = 1000L,
tags =
arrayOf(
arrayOf("e", JesterProtocol.START_POSITION_HASH),
),
content = content,
sig = "test_sig",
)
assertEquals(30, event.kind)
assertTrue(event.isStartEvent(), "Should be detected as start event")
assertFalse(event.isMoveEvent(), "Should not be a move event")
assertEquals(0, event.contentKind())
assertEquals(JesterProtocol.FEN_START, event.fen())
assertEquals(Color.WHITE, event.playerColor())
assertEquals("abc12345", event.nonce())
assertTrue(event.history().isEmpty(), "Start event has no history")
assertNull(event.opponentPubkey(), "Open challenge has no opponent")
}
@Test
fun `parse start event - private challenge`() {
val content = """{"version":"0","kind":0,"fen":"${JesterProtocol.FEN_START}","history":[],"nonce":"xyz98765","playerColor":"black"}"""
val event =
JesterEvent(
id = startEventId,
pubKey = testPubkey,
createdAt = 1000L,
tags =
arrayOf(
arrayOf("e", JesterProtocol.START_POSITION_HASH),
arrayOf("p", opponentPubkey),
),
content = content,
sig = "test_sig",
)
assertTrue(event.isStartEvent())
assertEquals(Color.BLACK, event.playerColor())
assertEquals(opponentPubkey, event.opponentPubkey(), "Private challenge should have opponent")
}
@Test
fun `start event e-tag references START_POSITION_HASH`() {
val content = """{"version":"0","kind":0,"fen":"${JesterProtocol.FEN_START}","history":[]}"""
val event =
JesterEvent(
id = startEventId,
pubKey = testPubkey,
createdAt = 1000L,
tags =
arrayOf(
arrayOf("e", JesterProtocol.START_POSITION_HASH),
),
content = content,
sig = "test_sig",
)
val eTags = event.eTags()
assertEquals(1, eTags.size)
assertEquals(JesterProtocol.START_POSITION_HASH, eTags[0], "Start event should reference START_POSITION_HASH")
}
// ==========================================================================
// MOVE EVENT TESTS
// ==========================================================================
@Test
fun `parse move event - first move e4`() {
val content = """{"version":"0","kind":1,"fen":"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1","move":"e4","history":["e4"]}"""
val event =
JesterEvent(
id = "move-001",
pubKey = testPubkey,
createdAt = 2000L,
tags =
arrayOf(
arrayOf("e", startEventId),
arrayOf("e", startEventId), // For first move, head is also start
arrayOf("p", opponentPubkey),
),
content = content,
sig = "test_sig",
)
assertFalse(event.isStartEvent(), "Should not be a start event")
assertTrue(event.isMoveEvent(), "Should be detected as move event")
assertEquals(1, event.contentKind())
assertEquals("e4", event.move())
assertEquals(listOf("e4"), event.history())
assertEquals("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1", event.fen())
assertEquals(opponentPubkey, event.opponentPubkey())
}
@Test
fun `parse move event - multiple moves in history`() {
val content = """{"version":"0","kind":1,"fen":"rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2","move":"Nf3","history":["e4","e5","Nf3"]}"""
val event =
JesterEvent(
id = "move-003",
pubKey = testPubkey,
createdAt = 4000L,
tags =
arrayOf(
arrayOf("e", startEventId),
arrayOf("e", "move-002"), // Previous move
arrayOf("p", opponentPubkey),
),
content = content,
sig = "test_sig",
)
assertTrue(event.isMoveEvent())
assertEquals("Nf3", event.move())
assertEquals(listOf("e4", "e5", "Nf3"), event.history())
assertEquals(3, event.history().size)
}
@Test
fun `move event e-tags structure - startEventId and headEventId`() {
val content = """{"version":"0","kind":1,"fen":"test","move":"e4","history":["e4"]}"""
val headEventId = "move-002"
val event =
JesterEvent(
id = "move-003",
pubKey = testPubkey,
createdAt = 3000L,
tags =
arrayOf(
arrayOf("e", startEventId),
arrayOf("e", headEventId),
arrayOf("p", opponentPubkey),
),
content = content,
sig = "test_sig",
)
assertEquals(startEventId, event.startEventId(), "First e-tag should be startEventId")
assertEquals(headEventId, event.headEventId(), "Second e-tag should be headEventId")
val eTags = event.eTags()
assertEquals(2, eTags.size)
assertEquals(startEventId, eTags[0])
assertEquals(headEventId, eTags[1])
}
// ==========================================================================
// GAME END EVENT TESTS
// ==========================================================================
@Test
fun `parse game end event - checkmate`() {
val content = """{"version":"0","kind":1,"fen":"r1bqkb1r/pppp1Qpp/2n2n2/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4","move":"Qxf7#","history":["e4","e5","Qh5","Nc6","Bc4","Nf6","Qxf7#"],"result":"1-0","termination":"checkmate"}"""
val event =
JesterEvent(
id = "move-007",
pubKey = testPubkey,
createdAt = 8000L,
tags =
arrayOf(
arrayOf("e", startEventId),
arrayOf("e", "move-006"),
arrayOf("p", opponentPubkey),
),
content = content,
sig = "test_sig",
)
assertTrue(event.isMoveEvent(), "End event is still a move event")
assertEquals("1-0", event.result(), "Should have result")
assertEquals("checkmate", event.termination(), "Should have termination reason")
assertEquals(7, event.history().size)
}
@Test
fun `parse game end event - resignation`() {
val content = """{"version":"0","kind":1,"fen":"test","move":"e5","history":["e4","e5"],"result":"0-1","termination":"resignation"}"""
val event =
JesterEvent(
id = "move-002",
pubKey = opponentPubkey,
createdAt = 3000L,
tags =
arrayOf(
arrayOf("e", startEventId),
arrayOf("e", "move-001"),
arrayOf("p", testPubkey),
),
content = content,
sig = "test_sig",
)
assertEquals("0-1", event.result(), "Black wins")
assertEquals("resignation", event.termination())
}
@Test
fun `parse game end event - draw`() {
val content = """{"version":"0","kind":1,"fen":"test","move":"Kf1","history":["e4","e5","Kf1"],"result":"1/2-1/2","termination":"draw_agreement"}"""
val event =
JesterEvent(
id = "move-003",
pubKey = testPubkey,
createdAt = 4000L,
tags =
arrayOf(
arrayOf("e", startEventId),
arrayOf("e", "move-002"),
arrayOf("p", opponentPubkey),
),
content = content,
sig = "test_sig",
)
assertEquals("1/2-1/2", event.result(), "Draw")
assertEquals("draw_agreement", event.termination())
}
// ==========================================================================
// EDGE CASES AND ERROR HANDLING
// ==========================================================================
@Test
fun `handle malformed JSON content gracefully`() {
val event =
JesterEvent(
id = "test",
pubKey = testPubkey,
createdAt = 1000L,
tags = emptyArray(),
content = "invalid json {{{}",
sig = "test_sig",
)
assertNull(event.contentKind(), "Should return null for invalid JSON")
assertFalse(event.isStartEvent(), "Should not crash on invalid content")
assertFalse(event.isMoveEvent(), "Should not crash on invalid content")
assertNull(event.fen())
assertNull(event.move())
assertTrue(event.history().isEmpty())
}
@Test
fun `handle empty content`() {
val event =
JesterEvent(
id = "test",
pubKey = testPubkey,
createdAt = 1000L,
tags = emptyArray(),
content = "",
sig = "test_sig",
)
assertNull(event.contentKind())
assertFalse(event.isStartEvent())
assertFalse(event.isMoveEvent())
}
@Test
fun `handle missing optional fields`() {
// Minimal valid content with only required fields
val content = """{"kind":1}"""
val event =
JesterEvent(
id = "test",
pubKey = testPubkey,
createdAt = 1000L,
tags = emptyArray(),
content = content,
sig = "test_sig",
)
assertEquals(1, event.contentKind())
assertNull(event.move())
assertTrue(event.history().isEmpty())
assertNull(event.result())
assertNull(event.termination())
assertNull(event.playerColor())
}
@Test
fun `handle event with no e-tags`() {
val content = """{"version":"0","kind":0}"""
val event =
JesterEvent(
id = "test",
pubKey = testPubkey,
createdAt = 1000L,
tags = emptyArray(),
content = content,
sig = "test_sig",
)
assertNull(event.startEventId(), "No e-tags means no startEventId")
assertNull(event.headEventId(), "No e-tags means no headEventId")
assertTrue(event.eTags().isEmpty())
}
@Test
fun `handle event with only one e-tag`() {
val content = """{"version":"0","kind":1}"""
val event =
JesterEvent(
id = "test",
pubKey = testPubkey,
createdAt = 1000L,
tags =
arrayOf(
arrayOf("e", startEventId),
),
content = content,
sig = "test_sig",
)
assertEquals(startEventId, event.startEventId())
assertNull(event.headEventId(), "Only one e-tag means no headEventId")
}
// ==========================================================================
// JESTER GAME EVENTS CONTAINER TESTS
// ==========================================================================
@Test
fun `JesterGameEvents - empty returns correct values`() {
val events = JesterGameEvents.empty()
assertNull(events.startEvent)
assertTrue(events.moves.isEmpty())
assertNull(events.latestMove())
assertEquals(JesterProtocol.FEN_START, events.currentFen())
assertTrue(events.fullHistory().isEmpty())
assertFalse(events.isEnded())
assertNull(events.result())
}
@Test
fun `JesterGameEvents - latestMove returns move with longest history`() {
val move1 = createTestMoveEvent("move-001", listOf("e4"))
val move2 = createTestMoveEvent("move-002", listOf("e4", "e5"))
val move3 = createTestMoveEvent("move-003", listOf("e4", "e5", "Nf3"))
// Provide moves in random order
val events =
JesterGameEvents(
startEvent = null,
moves = listOf(move2, move3, move1),
)
val latest = events.latestMove()
assertNotNull(latest)
assertEquals("move-003", latest.id, "Should return move with longest history")
assertEquals(3, latest.history().size)
}
@Test
fun `JesterGameEvents - currentFen returns FEN from latest move`() {
val move1 = createTestMoveEvent("move-001", listOf("e4"), fen = "fen-after-e4")
val move2 = createTestMoveEvent("move-002", listOf("e4", "e5"), fen = "fen-after-e5")
val events =
JesterGameEvents(
startEvent = null,
moves = listOf(move1, move2),
)
assertEquals("fen-after-e5", events.currentFen())
}
@Test
fun `JesterGameEvents - fullHistory returns history from latest move`() {
val move1 = createTestMoveEvent("move-001", listOf("e4"))
val move2 = createTestMoveEvent("move-002", listOf("e4", "e5"))
val events =
JesterGameEvents(
startEvent = null,
moves = listOf(move1, move2),
)
assertEquals(listOf("e4", "e5"), events.fullHistory())
}
@Test
fun `JesterGameEvents - isEnded detects result in latest move`() {
val normalMove = createTestMoveEvent("move-001", listOf("e4"))
val endMove = createTestMoveEventWithResult("move-002", listOf("e4", "Qxf7#"), result = "1-0")
val ongoingGame = JesterGameEvents(startEvent = null, moves = listOf(normalMove))
val finishedGame = JesterGameEvents(startEvent = null, moves = listOf(normalMove, endMove))
assertFalse(ongoingGame.isEnded())
assertTrue(finishedGame.isEnded())
assertEquals("1-0", finishedGame.result())
}
// ==========================================================================
// EXTENSION FUNCTION TESTS
// ==========================================================================
@Test
fun `isJesterEvent extension detects kind 30`() {
val jesterEvent =
JesterEvent(
id = "test",
pubKey = testPubkey,
createdAt = 1000L,
tags = emptyArray(),
content = "{}",
sig = "sig",
)
assertTrue(jesterEvent.isJesterEvent())
}
@Test
fun `toJesterEvent converts valid Event`() {
val event =
JesterEvent(
id = "test",
pubKey = testPubkey,
createdAt = 1000L,
tags = emptyArray(),
content = """{"kind":0}""",
sig = "sig",
)
val jesterEvent = event.toJesterEvent()
assertNotNull(jesterEvent)
assertEquals("test", jesterEvent.id)
}
// ==========================================================================
// HELPER FUNCTIONS
// ==========================================================================
private fun createTestMoveEvent(
id: String,
history: List<String>,
fen: String = "test-fen",
): JesterEvent {
val historyJson = history.joinToString(",") { "\"$it\"" }
val content = """{"version":"0","kind":1,"fen":"$fen","move":"${history.last()}","history":[$historyJson]}"""
return JesterEvent(
id = id,
pubKey = testPubkey,
createdAt = 1000L + history.size * 1000,
tags =
arrayOf(
arrayOf("e", startEventId),
arrayOf("e", "prev-move"),
arrayOf("p", opponentPubkey),
),
content = content,
sig = "sig",
)
}
private fun createTestMoveEventWithResult(
id: String,
history: List<String>,
result: String,
): JesterEvent {
val historyJson = history.joinToString(",") { "\"$it\"" }
val content = """{"version":"0","kind":1,"fen":"test-fen","move":"${history.last()}","history":[$historyJson],"result":"$result","termination":"checkmate"}"""
return JesterEvent(
id = id,
pubKey = testPubkey,
createdAt = 1000L + history.size * 1000,
tags =
arrayOf(
arrayOf("e", startEventId),
arrayOf("e", "prev-move"),
arrayOf("p", opponentPubkey),
),
content = content,
sig = "sig",
)
}
}
@@ -0,0 +1,454 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip64Chess
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
/**
* Comprehensive tests for PGN parsing per NIP-64 specification
*
* Tests cover:
* - PGN metadata extraction
* - Move parsing in Standard Algebraic Notation
* - Game result parsing
* - Comments and variations handling
* - Edge cases and error handling
*/
class PGNParserTest {
// Test 1: Parse minimal PGN (NIP-64 requirement: accept import format)
@Test
fun `parse minimal PGN with single move`() {
val pgn = "1. e4 *"
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess, "Should successfully parse minimal PGN")
val game = result.getOrThrow()
assertEquals(1, game.moves.size, "Should have 1 move")
assertEquals("e4", game.moves[0].san)
assertEquals(GameResult.IN_PROGRESS, game.result)
}
// Test 2: Parse complete game with metadata (NIP-64 requirement)
@Test
fun `parse PGN with full metadata tags`() {
val pgn =
"""
[Event "F/S Return Match"]
[Site "Belgrade, Serbia JUG"]
[Date "1992.11.04"]
[Round "29"]
[White "Fischer, Robert J."]
[Black "Spassky, Boris V."]
[Result "1/2-1/2"]
1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 1/2-1/2
""".trimIndent()
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
// Verify metadata extraction
assertEquals("F/S Return Match", game.event)
assertEquals("Belgrade, Serbia JUG", game.site)
assertEquals("1992.11.04", game.date)
assertEquals("29", game.round)
assertEquals("Fischer, Robert J.", game.white)
assertEquals("Spassky, Boris V.", game.black)
assertEquals(GameResult.DRAW, game.result)
// Verify moves
assertEquals(6, game.moves.size)
assertEquals("e4", game.moves[0].san)
assertEquals("Nf3", game.moves[2].san)
}
// Test 3: Scholar's Mate (4 move checkmate)
@Test
fun `parse scholars mate with checkmate notation`() {
val pgn =
"""
[Event "Scholar's Mate"]
[White "Alice"]
[Black "Bob"]
[Result "1-0"]
1. e4 e5 2. Bc4 Nc6 3. Qh5 Nf6 4. Qxf7# 1-0
""".trimIndent()
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
assertEquals(7, game.moves.size)
assertEquals(GameResult.WHITE_WINS, game.result)
// Verify last move has checkmate marker
val lastMove = game.moves.last()
assertEquals("Qxf7#", lastMove.san)
assertTrue(lastMove.isCheckmate, "Last move should be checkmate")
assertTrue(lastMove.isCapture, "Last move should be capture")
}
// Test 4: Fool's Mate (2 move checkmate)
@Test
fun `parse fools mate shortest checkmate`() {
val pgn =
"""
1. f3 e5 2. g4 Qh4# 0-1
""".trimIndent()
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
assertEquals(4, game.moves.size)
assertEquals(GameResult.BLACK_WINS, game.result)
val lastMove = game.moves.last()
assertEquals("Qh4#", lastMove.san)
assertTrue(lastMove.isCheckmate)
}
// Test 5: Castling notation
@Test
fun `parse castling moves kingside and queenside`() {
val pgn = "1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. O-O O-O 5. d3 d6 6. c3 a6 7. a4 a5 8. O-O-O *"
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
// Find castling moves
val kingsideCastling = game.moves.filter { it.san == "O-O" }
val queensideCastling = game.moves.filter { it.san == "O-O-O" }
assertEquals(2, kingsideCastling.size, "Should have 2 kingside castling moves")
assertEquals(1, queensideCastling.size, "Should have 1 queenside castling move")
kingsideCastling.forEach { move ->
assertTrue(move.isCastling, "O-O should be marked as castling")
assertEquals(PieceType.KING, move.piece)
}
queensideCastling.forEach { move ->
assertTrue(move.isCastling, "O-O-O should be marked as castling")
assertEquals(PieceType.KING, move.piece)
}
}
// Test 6: Pawn promotion
@Test
fun `parse pawn promotion to queen`() {
val pgn =
"""
[Event "Promotion Example"]
1. e4 d5 2. exd5 Qxd5 3. Nc3 Qa5 4. d4 c6 5. Nf3 Bg4 6. Bf4 e6
7. h3 Bxf3 8. Qxf3 Bb4 9. Be2 Nd7 10. a3 O-O-O 11. axb4 Qxa1+
12. Kd2 Qxh1 13. Qxh1 a6 14. c4 f6 15. b5 axb5 16. cxb5 c5
17. b6 Ne7 18. Qh2 h6 19. dxc5 Nxc5 20. b3 Kc8 21. Qg3 Ncd7
22. Bd6 Nf5 23. Qf4 Nxd6 24. Qxd6 Nb8 25. Kc3 Rh7 26. Kb4 Rd7
27. Qc5+ Kd8 28. Bf3 Ke8 29. Bd5 exd5 30. Nxd5 Kf7 31. b7 Kg6
32. b8=Q *
""".trimIndent()
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
// Find promotion move
val promotionMove = game.moves.firstOrNull { it.promotion != null }
assertNotNull(promotionMove, "Should have promotion move")
assertEquals(PieceType.QUEEN, promotionMove.promotion)
assertTrue(promotionMove.san.contains("=Q"), "Promotion move should contain =Q")
}
// Test 7: Captures
@Test
fun `parse capture notation`() {
val pgn = "1. e4 d5 2. exd5 Qxd5 3. Nc3 Qxd4 *"
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
val captures = game.moves.filter { it.isCapture }
assertEquals(3, captures.size, "Should have 3 capture moves")
captures.forEach { move ->
assertTrue(move.san.contains("x"), "Capture moves should contain 'x'")
}
}
// Test 8: Check notation
@Test
fun `parse check and checkmate markers`() {
val pgn = "1. e4 e5 2. Qh5 Nc6 3. Bc4 Nf6 4. Qxf7# 1-0"
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
val checkMoves = game.moves.filter { it.isCheck }
val checkmateMoves = game.moves.filter { it.isCheckmate }
assertTrue(checkmateMoves.isNotEmpty(), "Should have checkmate moves")
checkmateMoves.forEach { move ->
assertTrue(move.san.contains("#"), "Checkmate moves should contain #")
}
}
// Test 9: Comments and variations (NIP-64: should handle PGN comments)
@Test
fun `parse PGN with comments and variations stripped`() {
val pgn =
"""
1. e4 {Best by test} e5 (1...c5 2. Nf3) 2. Nf3 Nc6 {Developing} 3. Bb5 *
""".trimIndent()
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
// Comments and variations should be stripped
assertEquals(5, game.moves.size)
assertEquals("e4", game.moves[0].san)
assertEquals("e5", game.moves[1].san)
assertEquals("Nf3", game.moves[2].san)
}
// Test 10: NAG annotations (Numeric Annotation Glyphs)
@Test
fun `parse PGN with NAG annotations`() {
val pgn = "1. e4$1 e5$6 2. Nf3$10 *"
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
assertEquals(3, game.moves.size)
}
// Test 11: Disambiguating moves
@Test
fun `parse moves with disambiguation`() {
val pgn =
"""
1. Nf3 Nf6 2. Nc3 Nc6 3. d4 d5 4. Bf4 Bf5 5. e3 e6
6. Nbd2 Nbd7 7. Bd3 Bd6 *
""".trimIndent()
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
// Check for disambiguated moves (Nbd2, Nbd7)
val disambiguatedMoves = game.moves.filter { it.fromSquare != null }
assertTrue(disambiguatedMoves.isNotEmpty(), "Should have disambiguated moves")
}
// Test 12: All possible game results
@Test
fun `parse all game result notations`() {
val whiteWins = "1. e4 e5 2. Qh5 Nc6 3. Bc4 Nf6 4. Qxf7# 1-0"
val blackWins = "1. f3 e5 2. g4 Qh4# 0-1"
val draw = "1. e4 e5 2. Nf3 Nc6 1/2-1/2"
val inProgress = "1. e4 e5 *"
assertEquals(GameResult.WHITE_WINS, PGNParser.parse(whiteWins).getOrThrow().result)
assertEquals(GameResult.BLACK_WINS, PGNParser.parse(blackWins).getOrThrow().result)
assertEquals(GameResult.DRAW, PGNParser.parse(draw).getOrThrow().result)
assertEquals(GameResult.IN_PROGRESS, PGNParser.parse(inProgress).getOrThrow().result)
}
// Test 13: Empty/invalid PGN handling
@Test
fun `handle empty PGN gracefully`() {
val emptyPgn = ""
val result = PGNParser.parse(emptyPgn)
// Should not crash, might return empty game
assertTrue(result.isSuccess || result.isFailure)
}
// Test 14: Position generation
@Test
fun `generate positions for each move`() {
val pgn = "1. e4 e5 2. Nf3 Nc6 3. Bb5 *"
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
// Should have starting position + one per move
assertEquals(game.moves.size + 1, game.positions.size)
// First position should be starting position
val startPos = game.positions[0]
assertEquals(Color.WHITE, startPos.activeColor)
assertEquals(1, startPos.moveNumber)
}
// Test 15: Verify required metadata (NIP-64: PGN should have standard tags)
@Test
fun `detect presence of required PGN metadata tags`() {
val fullPgn =
"""
[Event "FIDE World Championship"]
[Site "London"]
[Date "2018.11.28"]
[Round "12"]
[White "Carlsen, Magnus"]
[Black "Caruana, Fabiano"]
[Result "1-0"]
1. e4 *
""".trimIndent()
val minimalPgn = "1. e4 *"
val fullGame = PGNParser.parse(fullPgn).getOrThrow()
val minimalGame = PGNParser.parse(minimalPgn).getOrThrow()
assertTrue(fullGame.hasRequiredMetadata(), "Full PGN should have required metadata")
assertFalse(minimalGame.hasRequiredMetadata(), "Minimal PGN should not have required metadata")
}
// Test 16: Long tournament game
@Test
fun `parse realistic tournament game`() {
val pgn =
"""
[Event "Wch"]
[Site "New York"]
[Date "1886.??.??"]
[Round "1"]
[White "Zukertort, Johannes"]
[Black "Steinitz, William"]
[Result "0-1"]
1. d4 d5 2. c4 e6 3. Nc3 Nf6 4. e3 c5 5. Nf3 Nc6 6. a3 dxc4
7. Bxc4 cxd4 8. exd4 Be7 9. O-O O-O 10. Qd3 Bd7 11. Qe2 Qb8
12. Rd1 Rd8 13. Be3 Be8 14. Ne5 Nxe5 15. dxe5 Rxd1+ 16. Rxd1 Nd7
17. f4 Nc5 18. Qf2 Rc8 19. b4 Na6 20. Bd3 Nb8 21. Ne4 Nc6
22. Nd6 Bxd6 23. exd6 Qxd6 24. Bxh7+ Kh8 25. Bf5 Qc7 26. Bxc8 Qxc8
27. Qd2 Bg6 28. Qd7 Qxd7 29. Rxd7 b6 30. Bc1 Nd8 31. Rxd8+ 0-1
""".trimIndent()
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
assertEquals("Zukertort, Johannes", game.white)
assertEquals("Steinitz, William", game.black)
assertEquals(GameResult.BLACK_WINS, game.result)
assertTrue(game.moves.size > 50, "Tournament game should have many moves")
}
// Test 17: Alternative castling notation (0-0 instead of O-O)
@Test
fun `parse alternative castling notation with zeros`() {
val pgn = "1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. 0-0 0-0 *"
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
val castlingMoves = game.moves.filter { it.isCastling }
assertEquals(2, castlingMoves.size)
}
// Test 18: Move count verification
@Test
fun `verify move numbers are correct`() {
val pgn = "1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 *"
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
assertEquals(6, game.moves.size)
// Verify move numbers
assertEquals(1, game.moves[0].moveNumber) // e4
assertEquals(1, game.moves[1].moveNumber) // e5
assertEquals(2, game.moves[2].moveNumber) // Nf3
assertEquals(2, game.moves[3].moveNumber) // Nc6
assertEquals(3, game.moves[4].moveNumber) // Bb5
assertEquals(3, game.moves[5].moveNumber) // a6
}
// Test 19: Move colors are correct
@Test
fun `verify move colors alternate correctly`() {
val pgn = "1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 *"
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
assertEquals(Color.WHITE, game.moves[0].color)
assertEquals(Color.BLACK, game.moves[1].color)
assertEquals(Color.WHITE, game.moves[2].color)
assertEquals(Color.BLACK, game.moves[3].color)
assertEquals(Color.WHITE, game.moves[4].color)
assertEquals(Color.BLACK, game.moves[5].color)
}
// Test 20: Piece type detection
@Test
fun `detect piece types from SAN notation`() {
val pgn = "1. e4 e5 2. Nf3 Nc6 3. Bb5 Bc5 4. Qa4 Qf6 5. Ke2 Ke7 6. Ra3 Ra6 *"
val result = PGNParser.parse(pgn)
assertTrue(result.isSuccess)
val game = result.getOrThrow()
// e4, e5 - pawns
assertEquals(PieceType.PAWN, game.moves[0].piece)
assertEquals(PieceType.PAWN, game.moves[1].piece)
// Nf3, Nc6 - knights
assertEquals(PieceType.KNIGHT, game.moves[2].piece)
assertEquals(PieceType.KNIGHT, game.moves[3].piece)
// Bb5, Bc5 - bishops
assertEquals(PieceType.BISHOP, game.moves[4].piece)
assertEquals(PieceType.BISHOP, game.moves[5].piece)
// Qa4, Qf6 - queens
assertEquals(PieceType.QUEEN, game.moves[6].piece)
assertEquals(PieceType.QUEEN, game.moves[7].piece)
// Ke2, Ke7 - kings
assertEquals(PieceType.KING, game.moves[8].piece)
assertEquals(PieceType.KING, game.moves[9].piece)
// Ra3, Ra6 - rooks
assertEquals(PieceType.ROOK, game.moves[10].piece)
assertEquals(PieceType.ROOK, game.moves[11].piece)
}
}
@@ -0,0 +1,113 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
import kotlin.test.assertTrue
class GZipTest {
@Test
fun roundTripSimpleString() {
val original = "Hello, Nostr!"
val decompressed = GZip.decompress(GZip.compress(original))
assertEquals(original, decompressed)
}
@Test
fun roundTripEmptyString() {
val original = ""
val decompressed = GZip.decompress(GZip.compress(original))
assertEquals(original, decompressed)
}
@Test
fun roundTripLongRepetitiveString() {
val original = "abcdefghij".repeat(1000)
val compressed = GZip.compress(original)
val decompressed = GZip.decompress(compressed)
assertEquals(original, decompressed)
}
@Test
fun compressedSizeIsSmallerForRepetitiveInput() {
val original = "abcdefghij".repeat(1000)
val compressed = GZip.compress(original)
assertTrue(
compressed.size < original.encodeToByteArray().size,
"Expected compressed size (${compressed.size}) to be smaller than original (${original.encodeToByteArray().size})",
)
}
@Test
fun roundTripUnicodeString() {
val original = "こんにちは世界 🌍 مرحبا Привет"
val decompressed = GZip.decompress(GZip.compress(original))
assertEquals(original, decompressed)
}
@Test
fun roundTripNostrJsonEvent() {
val original =
"""{"id":"abc123","pubkey":"deadbeef","created_at":1700000000,"kind":1,""" +
""""tags":[],"content":"Hello world","sig":"cafebabe"}"""
val decompressed = GZip.decompress(GZip.compress(original))
assertEquals(original, decompressed)
}
@Test
fun compressedBytesStartWithGzipMagicNumber() {
// gzip streams always begin with 0x1F 0x8B
val compressed = GZip.compress("test")
assertTrue(compressed.size >= 2, "Compressed output too short to contain gzip header")
assertEquals(0x1F.toByte(), compressed[0], "Expected gzip magic byte 0 (0x1F)")
assertEquals(0x8B.toByte(), compressed[1], "Expected gzip magic byte 1 (0x8B)")
}
@Test
fun compressedOutputDiffersFromInput() {
val original = "Hello, Nostr!"
val compressed = GZip.compress(original)
assertTrue(
!compressed.contentEquals(original.encodeToByteArray()),
"Compressed output should differ from the raw input bytes",
)
}
@Test
fun decompressInvalidDataThrows() {
val garbage = byteArrayOf(0x00, 0x01, 0x02, 0x03, 0x04)
assertFails { GZip.decompress(garbage) }
}
@Test
fun roundTripSingleCharacter() {
val original = "A"
assertEquals(original, GZip.decompress(GZip.compress(original)))
}
@Test
fun roundTripSpecialCharacters() {
val original = "\t\n\r\u0000\u001F\u007F"
assertEquals(original, GZip.decompress(GZip.compress(original)))
}
}
@@ -20,10 +20,28 @@
*/
package com.vitorpamplona.quartz.utils
import com.vitorpamplona.quartz.nip01Core.crypto.DeterministicSigner
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.EventAssembler
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
fun String.nsecToKeyPair() = KeyPair(this.bechToBytes())
fun String.nsecToSigner() = this.nsecToKeyPair().let { DeterministicSigner(it) }
class DeterministicSigner(
val key: KeyPair,
val pubKey: HexKey = key.pubKey.toHexKey(),
) {
fun <T : Event> sign(
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
): T = EventAssembler.hashAndSign(pubKey, createdAt, kind, tags, content, key.privKey!!, nonce = null)
fun <T : Event> sign(ev: EventTemplate<T>): T = sign(ev.createdAt, ev.kind, ev.tags, ev.content)
}