├── jitpack.yml
├── rhodium-core
├── .gitignore
├── src
│ ├── androidMain
│ │ ├── AndroidManifest.xml
│ │ └── kotlin
│ │ │ └── rhodium
│ │ │ ├── net
│ │ │ └── HttpClient.android.kt
│ │ │ ├── crypto
│ │ │ └── CryptoProvider.android.kt
│ │ │ └── android
│ │ │ └── NostrLifeCycle.kt
│ ├── commonMain
│ │ └── kotlin
│ │ │ └── rhodium
│ │ │ ├── net
│ │ │ ├── HttpClient.kt
│ │ │ └── NostrUtils.kt
│ │ │ ├── crypto
│ │ │ ├── CryptoProvider.kt
│ │ │ ├── Identity.kt
│ │ │ ├── tlv
│ │ │ │ ├── entity
│ │ │ │ │ ├── Entity.kt
│ │ │ │ │ ├── NSec.kt
│ │ │ │ │ ├── Note.kt
│ │ │ │ │ ├── NPub.kt
│ │ │ │ │ ├── NRelay.kt
│ │ │ │ │ ├── EntityUtils.kt
│ │ │ │ │ ├── NProfile.kt
│ │ │ │ │ ├── NEvent.kt
│ │ │ │ │ └── NAddress.kt
│ │ │ │ ├── TlvTypes.kt
│ │ │ │ ├── TlvBuilder.kt
│ │ │ │ └── Tlv.kt
│ │ │ ├── CryptoUtils.kt
│ │ │ ├── Nip19Parser.kt
│ │ │ └── Bech32Util.kt
│ │ │ ├── logging
│ │ │ └── Logging.kt
│ │ │ ├── DateTimeUtils.kt
│ │ │ └── nostr
│ │ │ ├── events
│ │ │ └── MetadataEvent.kt
│ │ │ ├── relay
│ │ │ ├── info
│ │ │ │ ├── Preferences.kt
│ │ │ │ ├── RetentionPolicy.kt
│ │ │ │ ├── RelayLimits.kt
│ │ │ │ └── Payments.kt
│ │ │ ├── RelayPool.kt
│ │ │ └── Relay.kt
│ │ │ ├── NostrErrors.kt
│ │ │ ├── Tag.kt
│ │ │ ├── NostrFilter.kt
│ │ │ ├── EventExt.kt
│ │ │ ├── Events.kt
│ │ │ ├── Event.kt
│ │ │ └── client
│ │ │ └── ClientMessage.kt
│ ├── linuxMain
│ │ ├── cinterop
│ │ │ └── libs.def
│ │ └── kotlin
│ │ │ └── rhodium
│ │ │ ├── crypto
│ │ │ └── CryptoProvider.linux.kt
│ │ │ └── net
│ │ │ └── HttpClient.linux.kt
│ ├── commonJvmMain
│ │ └── kotlin
│ │ │ └── rhodium
│ │ │ ├── net
│ │ │ └── HttpClient.commonJvm.kt
│ │ │ └── crypto
│ │ │ └── CryptoProvider.commonJvm.kt
│ ├── appleMain
│ │ └── kotlin
│ │ │ └── rhodium
│ │ │ ├── net
│ │ │ └── HttpClient.apple.kt
│ │ │ └── crypto
│ │ │ └── CryptoProvider.apple.kt
│ ├── androidUnitTest
│ │ └── kotlin
│ │ │ └── rhodium
│ │ │ └── android
│ │ │ └── ExampleUnitTest.kt
│ ├── androidInstrumentedTest
│ │ └── kotlin
│ │ │ └── rhodium
│ │ │ └── android
│ │ │ └── ExampleInstrumentedTest.kt
│ └── commonTest
│ │ └── kotlin
│ │ └── rhodium
│ │ ├── crypto
│ │ └── CryptoUtilsTest.kt
│ │ └── nostr
│ │ ├── relay
│ │ ├── RelayInfoTests.kt
│ │ └── RelayMessageTests.kt
│ │ ├── EventTests.kt
│ │ ├── NostrTests.kt
│ │ ├── client
│ │ └── ClientMessageTests.kt
│ │ └── NostrFilterTest.kt
├── consumer-rules.pro
├── proguard-rules.pro
└── build.gradle.kts
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── settings.gradle.kts
├── gradle.properties
├── LICENSE
├── .github
└── workflows
│ ├── publish.yml
│ └── gradle.yml
├── gradlew.bat
├── gradlew
├── .gitignore
└── README.md
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - openjdk17
3 |
--------------------------------------------------------------------------------
/rhodium-core/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KotlinGeekDev/Rhodium/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/rhodium-core/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/net/HttpClient.kt:
--------------------------------------------------------------------------------
1 | package rhodium.net
2 |
3 | import io.ktor.client.*
4 |
5 | internal expect fun httpClient(config: HttpClientConfig<*>.() -> Unit = {}): HttpClient
--------------------------------------------------------------------------------
/rhodium-core/src/linuxMain/cinterop/libs.def:
--------------------------------------------------------------------------------
1 | # headers = curl/curl.h
2 | # headerFilter = curl/*
3 |
4 | compilerOpts.linux = -I/usr/include -I/usr/include/x86_64-linux-gnu
5 | linkerOpts.linux = -L/usr/lib/x86_64-linux-gnu
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Sep 03 20:31:36 WAT 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/rhodium-core/src/androidMain/kotlin/rhodium/net/HttpClient.android.kt:
--------------------------------------------------------------------------------
1 | package rhodium.net
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.engine.okhttp.*
5 |
6 | internal actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(OkHttp) {
7 | config(this)
8 |
9 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonJvmMain/kotlin/rhodium/net/HttpClient.commonJvm.kt:
--------------------------------------------------------------------------------
1 | package rhodium.net
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.engine.okhttp.*
5 |
6 | internal actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(OkHttp) {
7 | config(this)
8 |
9 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/CryptoProvider.kt:
--------------------------------------------------------------------------------
1 | package rhodium.crypto
2 |
3 | import dev.whyoleg.cryptography.CryptographyProvider
4 | import dev.whyoleg.cryptography.random.CryptographyRandom
5 |
6 | expect fun SecureRandom(): CryptographyRandom
7 | expect fun getCryptoProvider(): CryptographyProvider
--------------------------------------------------------------------------------
/rhodium-core/src/appleMain/kotlin/rhodium/net/HttpClient.apple.kt:
--------------------------------------------------------------------------------
1 | package rhodium.net
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.engine.darwin.*
5 |
6 | internal actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(Darwin) {
7 |
8 | config(this)
9 | engine {
10 |
11 | }
12 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 |
9 | dependencyResolutionManagement {
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 |
16 | rootProject.name = "rhodium"
17 | include("rhodium-core")
18 | //include(":kostr-android")
19 |
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/logging/Logging.kt:
--------------------------------------------------------------------------------
1 | package rhodium.logging
2 |
3 | import co.touchlab.kermit.DefaultFormatter
4 | import co.touchlab.kermit.Logger
5 | import co.touchlab.kermit.loggerConfigInit
6 | import co.touchlab.kermit.platformLogWriter
7 |
8 | val serviceLogger = Logger(
9 | config = loggerConfigInit(platformLogWriter(DefaultFormatter)),
10 | tag = "RhodiumLogger"
11 | )
--------------------------------------------------------------------------------
/rhodium-core/src/appleMain/kotlin/rhodium/crypto/CryptoProvider.apple.kt:
--------------------------------------------------------------------------------
1 | package rhodium.crypto
2 |
3 | import dev.whyoleg.cryptography.CryptographyProvider
4 | import dev.whyoleg.cryptography.providers.apple.Apple
5 | import dev.whyoleg.cryptography.random.CryptographyRandom
6 |
7 | actual fun SecureRandom(): CryptographyRandom {
8 | return CryptographyRandom
9 | }
10 |
11 | actual fun getCryptoProvider(): CryptographyProvider {
12 | return CryptographyProvider.Apple
13 | }
--------------------------------------------------------------------------------
/rhodium-core/src/linuxMain/kotlin/rhodium/crypto/CryptoProvider.linux.kt:
--------------------------------------------------------------------------------
1 | package rhodium.crypto
2 |
3 | import dev.whyoleg.cryptography.CryptographyProvider
4 | import dev.whyoleg.cryptography.providers.openssl3.Openssl3
5 | import dev.whyoleg.cryptography.random.CryptographyRandom
6 |
7 | actual fun SecureRandom() : CryptographyRandom {
8 | return CryptographyRandom
9 | }
10 |
11 | actual fun getCryptoProvider(): CryptographyProvider {
12 | return CryptographyProvider.Openssl3
13 | }
--------------------------------------------------------------------------------
/rhodium-core/src/androidUnitTest/kotlin/rhodium/android/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package rhodium.android
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | // println("Decoded profile: ${someProfile.toString()}")
15 | assertEquals(4, 2 + 2)
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/rhodium-core/src/androidMain/kotlin/rhodium/crypto/CryptoProvider.android.kt:
--------------------------------------------------------------------------------
1 | package rhodium.crypto
2 |
3 | import dev.whyoleg.cryptography.CryptographyProvider
4 | import dev.whyoleg.cryptography.providers.jdk.JDK
5 | import dev.whyoleg.cryptography.random.CryptographyRandom
6 | import dev.whyoleg.cryptography.random.asCryptographyRandom
7 | import java.security.SecureRandom
8 |
9 | actual fun SecureRandom() : CryptographyRandom = SecureRandom().asCryptographyRandom()
10 |
11 | actual fun getCryptoProvider(): CryptographyProvider {
12 | return CryptographyProvider.JDK
13 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonJvmMain/kotlin/rhodium/crypto/CryptoProvider.commonJvm.kt:
--------------------------------------------------------------------------------
1 | package rhodium.crypto
2 |
3 | import dev.whyoleg.cryptography.CryptographyProvider
4 | import dev.whyoleg.cryptography.providers.jdk.JDK
5 | import dev.whyoleg.cryptography.random.CryptographyRandom
6 | import dev.whyoleg.cryptography.random.asCryptographyRandom
7 | import java.security.SecureRandom
8 |
9 | actual fun SecureRandom() : CryptographyRandom = SecureRandom().asCryptographyRandom()
10 |
11 | actual fun getCryptoProvider(): CryptographyProvider {
12 | return CryptographyProvider.JDK
13 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
3 | #For Gradle
4 | org.gradle.caching=true
5 | org.gradle.configuration-cache=true
6 | org.gradle.warning.mode=all
7 |
8 | #For Android
9 | android.useAndroidX=true
10 | android.enableR8.fullMode=true
11 | android.nonTransitiveRClass=true
12 | # Automatically convert third-party libraries to use AndroidX
13 | android.enableJetifier=true
14 |
15 | # For K/N
16 | kotlin.mpp.enableCInteropCommonization=true
17 | kotlin.native.cacheKind.linuxX64=none
18 |
19 | # For JVM and Android friendliness
20 | kotlin.publishJvmEnvironmentAttribute=true
21 |
22 | org.gradle.jvmargs=-XX:MaxMetaspaceSize=1G
--------------------------------------------------------------------------------
/rhodium-core/consumer-rules.pro:
--------------------------------------------------------------------------------
1 | # For more details, see
2 | # http://developer.android.com/guide/developing/tools/proguard.html
3 |
4 | # preserve the line number information for debugging stack traces.
5 | -dontobfuscate
6 | -keepattributes LocalVariableTable
7 | -keepattributes LocalVariableTypeTable
8 | -keepattributes *Annotation*
9 | -keepattributes SourceFile
10 | -keepattributes LineNumberTable
11 | -keepattributes Signature
12 | -keepattributes Exceptions
13 | -keepattributes InnerClasses
14 | -keepattributes EnclosingMethod
15 | -keepattributes MethodParameters
16 | -keepparameternames
17 |
18 |
19 | # Keep all names
20 | -keepnames class ** { *; }
21 |
22 | # Keep All enums
23 | -keep enum ** { *; }
24 |
25 | # preserve access to native classses
26 | -keep class fr.acinq.secp256k1.** { *; }
--------------------------------------------------------------------------------
/rhodium-core/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # For more details, see
2 | # http://developer.android.com/guide/developing/tools/proguard.html
3 |
4 | # preserve the line number information for debugging stack traces.
5 | -dontobfuscate
6 | -keepattributes LocalVariableTable
7 | -keepattributes LocalVariableTypeTable
8 | -keepattributes *Annotation*
9 | -keepattributes SourceFile
10 | -keepattributes LineNumberTable
11 | -keepattributes Signature
12 | -keepattributes Exceptions
13 | -keepattributes InnerClasses
14 | -keepattributes EnclosingMethod
15 | -keepattributes MethodParameters
16 | -keepparameternames
17 |
18 |
19 | # Keep all names
20 | -keepnames class ** { *; }
21 |
22 | # Keep All enums
23 | -keep enum ** { *; }
24 |
25 | # preserve access to native classses
26 | -keep class fr.acinq.secp256k1.** { *; }
27 |
--------------------------------------------------------------------------------
/rhodium-core/src/androidInstrumentedTest/kotlin/rhodium/android/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package rhodium.android
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | /**
10 | * Instrumented test, which will execute on an Android device.
11 | *
12 | * See [testing documentation](http://d.android.com/tools/testing).
13 | */
14 | @RunWith(AndroidJUnit4::class)
15 | class ExampleInstrumentedTest {
16 | @Test
17 | fun useAppContext() {
18 | // val someProfile = AndroidClient()
19 | // someProfile.testProfile()
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("ktnostr.android.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/DateTimeUtils.kt:
--------------------------------------------------------------------------------
1 | package rhodium
2 |
3 | import kotlinx.datetime.Clock
4 | import kotlinx.datetime.Instant
5 | import kotlinx.datetime.TimeZone
6 | import kotlinx.datetime.toLocalDateTime
7 |
8 | /**
9 | * The function takes a Unix timestamp in and returns
10 | * a human-readable date and time.
11 | * @param timestamp The Unix timestamp as a Long
12 | * @return A human-readable date and time, as a string.
13 | */
14 |
15 | fun formattedDateTime(timestamp: Long): String {
16 | return Instant.fromEpochSeconds(timestamp)
17 | .toLocalDateTime(TimeZone.currentSystemDefault()).toString()
18 |
19 |
20 | // return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault())
21 | // .format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
22 | }
23 |
24 |
25 | /**
26 | * The function below returns the current Unix timestamp.
27 | */
28 | fun currentSystemTimestamp(): Long = Clock.System.now().epochSeconds
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/nostr/events/MetadataEvent.kt:
--------------------------------------------------------------------------------
1 | package rhodium.nostr.events
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 | import rhodium.nostr.Event
6 | import rhodium.nostr.EventKind
7 | import rhodium.nostr.Tag
8 | import rhodium.nostr.eventMapper
9 |
10 |
11 | class MetadataEvent(
12 | id: String,
13 | pubkey: String,
14 | creationDate: Long,
15 | tags: List,
16 | content: String,
17 | signature: String
18 | ): Event(id, pubkey, creationDate, eventKind = EventKind.METADATA.kind, tags, content, signature) {
19 |
20 | fun userInfo(): UserInfo = eventMapper.decodeFromString(content)
21 |
22 | }
23 |
24 | @Serializable()
25 | data class UserInfo(
26 | val name: String,
27 | @SerialName("display_name") val displayName: String? = null,
28 | val about: String?,
29 | val picture: String? = null,
30 | val banner: String? = null,
31 | @SerialName("nip05") val address: String? = null,
32 | val website: String? = null,
33 | )
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 NullKtDev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Publish
3 | on:
4 | # push:
5 | # branches: ["develop"]
6 | release:
7 | types: [released, prereleased]
8 |
9 | concurrency:
10 | group: ${{ github.ref }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | publish:
15 | name: Release build and publish
16 | runs-on: macOS-latest
17 | steps:
18 | - name: Check out code
19 | uses: actions/checkout@v4
20 |
21 | - name: Set up JDK 17
22 | uses: actions/setup-java@v4
23 | with:
24 | distribution: 'temurin'
25 | java-version: 17
26 | - name: Publish to MavenCentral
27 | run: ./gradlew publishToMavenCentral --no-configuration-cache
28 | env:
29 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
30 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
31 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }}
32 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
33 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY_CONTENTS }}
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/Identity.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is copied(and modified) from https://github.com/Giszmo/NostrPostr.
3 | * Credits: Giszmo(on Github)
4 | */
5 |
6 | package rhodium.crypto
7 |
8 | class Identity(
9 | privKey: ByteArray? = null,
10 | pubKey: ByteArray? = null
11 | ) {
12 | val privKey: ByteArray?
13 | val pubKey: ByteArray
14 |
15 | init {
16 | if (privKey == null) {
17 | if (pubKey == null) {
18 | // create new, random keys
19 | this.privKey = CryptoUtils.generatePrivateKey()
20 | this.pubKey = CryptoUtils.getPublicKey(this.privKey)
21 | } else {
22 | // this is a read-only account
23 | check(pubKey.size == 32)
24 | this.privKey = null
25 | this.pubKey = pubKey
26 | }
27 | } else {
28 | // as private key is provided, ignore the public key and set keys according to private key
29 | this.privKey = privKey
30 | this.pubKey = CryptoUtils.getPublicKey(privKey)
31 | }
32 | }
33 |
34 | override fun toString(): String {
35 | return "Persona(privateKey=${privKey?.toHexString()}, publicKey=${pubKey.toHexString()})"
36 | }
37 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/nostr/relay/info/Preferences.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2025 KotlinGeekDev
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | *
24 | */
25 |
26 | package rhodium.nostr.relay.info
27 |
28 | import kotlinx.serialization.Serializable
29 |
30 | @Serializable()
31 | class Preferences
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/nostr/NostrErrors.kt:
--------------------------------------------------------------------------------
1 | package rhodium.nostr
2 |
3 | //The general class of Nostr Errors.
4 | sealed class NostrException : RuntimeException {
5 | constructor() : super()
6 | constructor(message: String?) : super(message)
7 | }
8 |
9 | /**
10 | * This error is thrown when something goes wrong during the Nip05 parsing process.
11 | */
12 | class Nip05ValidationError(override val message: String) : NostrException(message)
13 |
14 | /**
15 | * This error type is used when an event is invalid.
16 | * For example, when the event id is not valid, or
17 | * when the event signature is not valid.
18 | */
19 | class EventValidationError(override val message: String) : NostrException(message)
20 |
21 | /**
22 | * This error type is used when we receive an event whose kind we
23 | * do not understand and/or support.
24 | * This is probably an indication that it(the kind) needs to be supported.
25 | */
26 | class UnsupportedKindError(override val message: String) : NostrException(message)
27 |
28 | /**
29 | * For handling relay and relay-related errors.
30 | * For example, to be used when sending data to a relay,
31 | * or for relay connection management.
32 | */
33 | open class RelayError(override val message: String) : NostrException(message)
34 |
35 | class RelayMessageError(override val message: String) : RelayError(message)
36 |
37 | class RelayInfoFetchError(override val message: String) : RelayError(message)
38 |
39 |
40 |
--------------------------------------------------------------------------------
/rhodium-core/src/commonTest/kotlin/rhodium/crypto/CryptoUtilsTest.kt:
--------------------------------------------------------------------------------
1 | package rhodium.crypto
2 | import kotlin.test.Test
3 | import kotlin.test.assertEquals
4 |
5 |
6 | class CryptoUtilsTest {
7 | @Test
8 | fun testGetPublicKey() {
9 | val secKeyHex = "6ba903b7888191180a0959a6d286b9d0719d33a47395c519ba107470412d2069"
10 | val pubKeyHex = "8565b1a5a63ae21689b80eadd46f6493a3ed393494bb19d0854823a757d8f35f"
11 | val secKeyBytes = secKeyHex.toBytes()
12 | val actualPubKeyBytes = CryptoUtils.getPublicKey(secKeyBytes)
13 | val actualPubKeyHex = actualPubKeyBytes.toHexString()
14 | assertEquals(pubKeyHex, actualPubKeyHex, "PubKeys do not match!")
15 | }
16 |
17 | @Test
18 | fun testContentHash() {
19 | val content = "Kotlin"
20 | val hashHex = "c78f6c97923e81a2f04f09c5e87b69e085c1e47066a1136b5f590bfde696e2eb"
21 | val actualHashHex = CryptoUtils.contentHash(content).toHexString()
22 | assertEquals(hashHex, actualHashHex, "Hashes do not match!")
23 | }
24 |
25 | @Test
26 | fun `the kotlin stdlib byte conversion is the same as secp256k1-kmp`(){
27 | val pubKeyHex = "8565b1a5a63ae21689b80eadd46f6493a3ed393494bb19d0854823a757d8f35f"
28 | val secpBytes = pubKeyHex.toBytes()
29 | val stdlibBytes = pubKeyHex.encodeToByteArray()
30 | val secpString = secpBytes.toHexString()
31 | val stdlibString = stdlibBytes.decodeToString()
32 | assertEquals(secpString, stdlibString, "Do not match!")
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/rhodium-core/src/linuxMain/kotlin/rhodium/net/HttpClient.linux.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2025 KotlinGeekDev
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | *
24 | */
25 |
26 | package rhodium.net
27 |
28 | import io.ktor.client.*
29 | import io.ktor.client.engine.curl.Curl
30 |
31 | internal actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(Curl) {
32 | config(this)
33 | engine {
34 |
35 | }
36 | }
--------------------------------------------------------------------------------
/rhodium-core/src/androidMain/kotlin/rhodium/android/NostrLifeCycle.kt:
--------------------------------------------------------------------------------
1 | package rhodium.android
2 |
3 | import androidx.lifecycle.*
4 | import androidx.lifecycle.Lifecycle.State
5 |
6 | class NostrLifeCycle: LifecycleOwner {
7 | private var lifecycleState = State.INITIALIZED
8 | private val lifecycleRegistry = LifecycleRegistry(this)
9 |
10 | override val lifecycle: Lifecycle = lifecycleRegistry
11 |
12 | fun currentState(): Lifecycle.State {
13 | return lifecycleState
14 | }
15 |
16 |
17 | fun changeState(newState: Lifecycle.State){
18 | lifecycleState = newState
19 | }
20 |
21 |
22 | inner class NostrEventObserver() : DefaultLifecycleObserver, LifecycleEventObserver {
23 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
24 | val state = source.lifecycle.currentState
25 | this@NostrLifeCycle.changeState(state)
26 | this@NostrLifeCycle.lifecycleRegistry.handleLifecycleEvent(event)
27 | }
28 |
29 | override fun onCreate(owner: LifecycleOwner) {
30 | this@NostrLifeCycle.lifecycleRegistry.addObserver(this)
31 | super.onCreate(owner)
32 | }
33 |
34 | override fun onStart(owner: LifecycleOwner) {
35 | super.onStart(owner)
36 | }
37 |
38 | override fun onResume(owner: LifecycleOwner) {
39 | super.onResume(owner)
40 | }
41 |
42 | override fun onPause(owner: LifecycleOwner) {
43 | super.onPause(owner)
44 | }
45 |
46 | override fun onStop(owner: LifecycleOwner) {
47 | super.onStop(owner)
48 | }
49 |
50 | override fun onDestroy(owner: LifecycleOwner) {
51 | this@NostrLifeCycle.lifecycleRegistry.removeObserver(this)
52 | super.onDestroy(owner)
53 | }
54 | }
55 |
56 | }
--------------------------------------------------------------------------------
/.github/workflows/gradle.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub.
2 | # They are provided by a third-party and are governed by
3 | # separate terms of service, privacy policy, and support
4 | # documentation.
5 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
6 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
7 |
8 | name: KMP CI with Gradle
9 |
10 | on:
11 | push:
12 | branches: [ "develop" ]
13 | pull_request:
14 | branches: [ "develop" ]
15 | workflow_call:
16 |
17 | permissions:
18 | contents: read
19 |
20 | jobs:
21 | build:
22 | strategy:
23 | matrix:
24 | include:
25 | - target: iosSimulatorArm64Test
26 | os: macos-latest
27 | - target: commonJvmTest
28 | os: ubuntu-latest
29 | - target: linuxTest
30 | os: ubuntu-latest
31 | - target: testDebugUnitTest
32 | os: ubuntu-latest
33 | - target: testReleaseUnitTest
34 | os: ubuntu-latest
35 | runs-on: ${{ matrix.os }}
36 |
37 | steps:
38 | - uses: actions/checkout@v3
39 | - name: Validate Gradle Wrapper
40 | uses: gradle/actions/wrapper-validation@v3
41 | - uses: actions/cache@v3
42 | with:
43 | path: |
44 | ~/.konan
45 | key: ${{ runner.os }}-${{ hashFiles('**/.lock') }}
46 | - name: Set up JDK 17
47 | uses: actions/setup-java@v3
48 | with:
49 | java-version: '17'
50 | distribution: 'temurin'
51 | - name: Build with Gradle
52 | uses: gradle/gradle-build-action@ce999babab2de1c4b649dc15f0ee67e6246c994f
53 | with:
54 | arguments: ${{ matrix.target }} --no-configuration-cache
55 |
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/tlv/entity/Entity.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 KotlinGeekDev
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | *
22 | */
23 |
24 | package rhodium.crypto.tlv.entity
25 |
26 | /**
27 | * Copyright (c) 2024 Vitor Pamplona
28 | *
29 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
30 | * this software and associated documentation files (the "Software"), to deal in
31 | * the Software without restriction, including without limitation the rights to use,
32 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
33 | * Software, and to permit persons to whom the Software is furnished to do so,
34 | * subject to the following conditions:
35 | *
36 | * The above copyright notice and this permission notice shall be included in all
37 | * copies or substantial portions of the Software.
38 | *
39 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
41 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
42 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
43 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
44 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45 | */
46 |
47 | interface Entity
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/tlv/entity/NSec.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 KotlinGeekDev
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | *
22 | */
23 |
24 | package rhodium.crypto.tlv.entity
25 |
26 | /**
27 | * Copyright (c) 2024 Vitor Pamplona
28 | *
29 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
30 | * this software and associated documentation files (the "Software"), to deal in
31 | * the Software without restriction, including without limitation the rights to use,
32 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
33 | * Software, and to permit persons to whom the Software is furnished to do so,
34 | * subject to the following conditions:
35 | *
36 | * The above copyright notice and this permission notice shall be included in all
37 | * copies or substantial portions of the Software.
38 | *
39 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
41 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
42 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
43 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
44 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45 | */
46 |
47 | import rhodium.crypto.toHexString
48 |
49 | data class NSec(
50 | val hex: String,
51 | ) : Entity {
52 | companion object {
53 | fun parse(bytes: ByteArray): NSec? {
54 | if (bytes.isEmpty()) return null
55 | return NSec(bytes.toHexString())
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/tlv/TlvTypes.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 KotlinGeekDev
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | *
22 | */
23 |
24 | package rhodium.crypto.tlv
25 |
26 | /**
27 | * Copyright (c) 2024 Vitor Pamplona
28 | *
29 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
30 | * this software and associated documentation files (the "Software"), to deal in
31 | * the Software without restriction, including without limitation the rights to use,
32 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
33 | * Software, and to permit persons to whom the Software is furnished to do so,
34 | * subject to the following conditions:
35 | *
36 | * The above copyright notice and this permission notice shall be included in all
37 | * copies or substantial portions of the Software.
38 | *
39 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
41 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
42 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
43 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
44 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45 | */
46 |
47 |
48 | enum class TlvTypes(
49 | val id: Byte,
50 | ) {
51 | SPECIAL(0),
52 | RELAY(1),
53 | AUTHOR(2),
54 | KIND(3),
55 | }
56 |
57 | fun Tlv.firstAsInt(type: TlvTypes) = firstAsInt(type.id)
58 |
59 | fun Tlv.firstAsHex(type: TlvTypes) = firstAsHex(type.id)
60 |
61 | fun Tlv.firstAsString(type: TlvTypes) = firstAsString(type.id)
62 |
63 | fun Tlv.asStringList(type: TlvTypes) = asStringList(type.id)
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/tlv/entity/Note.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 KotlinGeekDev
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | *
22 | */
23 |
24 | package rhodium.crypto.tlv.entity
25 |
26 | /**
27 | * Copyright (c) 2024 Vitor Pamplona
28 | *
29 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
30 | * this software and associated documentation files (the "Software"), to deal in
31 | * the Software without restriction, including without limitation the rights to use,
32 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
33 | * Software, and to permit persons to whom the Software is furnished to do so,
34 | * subject to the following conditions:
35 | *
36 | * The above copyright notice and this permission notice shall be included in all
37 | * copies or substantial portions of the Software.
38 | *
39 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
41 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
42 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
43 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
44 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45 | */
46 |
47 | import rhodium.crypto.toBytes
48 | import rhodium.crypto.toHexString
49 |
50 | data class Note(
51 | val hex: String,
52 | ) : Entity {
53 | companion object {
54 | fun parse(bytes: ByteArray): Note? {
55 | if (bytes.isEmpty()) return null
56 | return Note(bytes.toHexString())
57 | }
58 |
59 | fun create(eventId: String): String = eventId.toBytes().toNote()
60 | }
61 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonTest/kotlin/rhodium/nostr/relay/RelayInfoTests.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2025 KotlinGeekDev
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | *
24 | */
25 |
26 | package rhodium.nostr.relay
27 |
28 | import kotlinx.coroutines.test.runTest
29 | import rhodium.nostr.relay.info.PaymentInfo
30 | import rhodium.nostr.relay.info.Payments
31 | import rhodium.nostr.relay.info.RelayLimits
32 | import kotlin.test.Test
33 | import kotlin.test.assertEquals
34 |
35 | class RelayInfoTests {
36 |
37 |
38 | @Test
39 | fun generatedAndManualRelayInfoAreTheSame() = runTest {
40 | val edenNostrLandInfo = Relay.Info(
41 | description = "✨ the leading Nostr relay powered by NFDB\n\uD83C\uDF10 connected to fi-transitory-01",
42 | name = "✨ nostr.land",
43 | contact = "",
44 | icon = "https://i.nostr.build/j6xguiCQRrdk6MsL.jpg",
45 | pubkey = "52b4a076bcbbbdc3a1aefa3735816cf74993b1b8db202b01c883c58be7fad8bd",
46 | relaySoftware = "NFDB",
47 | softwareVersion = "2.0 γ5",
48 | termsOfService = "https://nostr.land/terms",
49 | supportedNips = intArrayOf(),
50 | limits = RelayLimits(
51 | maxMessageLength = 65535,
52 | maxEventTagNumber = 2000,
53 | maxSubscriptions = 200,
54 | isAuthRequired = false,
55 | isPaymentRequired = true,
56 | ),
57 | paymentUrl = "https://nostr.land",
58 | paymentInfo = null
59 | )
60 |
61 | val obtainedAndParsedInfo = Relay.fetchInfoFor("wss://eden.nostr.land")
62 | println("Obtained relay info: ")
63 | println(obtainedAndParsedInfo)
64 |
65 | assertEquals(edenNostrLandInfo, obtainedAndParsedInfo)
66 | }
67 |
68 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/tlv/entity/NPub.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 KotlinGeekDev
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | *
22 | */
23 |
24 | package rhodium.crypto.tlv.entity
25 |
26 | /**
27 | * Copyright (c) 2024 Vitor Pamplona
28 | *
29 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
30 | * this software and associated documentation files (the "Software"), to deal in
31 | * the Software without restriction, including without limitation the rights to use,
32 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
33 | * Software, and to permit persons to whom the Software is furnished to do so,
34 | * subject to the following conditions:
35 | *
36 | * The above copyright notice and this permission notice shall be included in all
37 | * copies or substantial portions of the Software.
38 | *
39 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
41 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
42 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
43 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
44 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45 | */
46 |
47 | import rhodium.crypto.toBytes
48 | import rhodium.crypto.toHexString
49 |
50 | data class NPub(
51 | val hex: String,
52 | ) : Entity {
53 | companion object {
54 | fun parse(bytes: ByteArray): NPub? {
55 | if (bytes.isEmpty()) return null
56 | return NPub(bytes.toHexString())
57 | }
58 |
59 | fun create(authorPubKeyHex: String): String = authorPubKeyHex.toBytes().toNpub()
60 | }
61 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/tlv/entity/NRelay.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 KotlinGeekDev
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | *
22 | */
23 |
24 | package rhodium.crypto.tlv.entity
25 |
26 | import rhodium.crypto.tlv.Tlv
27 | import rhodium.crypto.tlv.TlvTypes
28 |
29 | /**
30 | * Copyright (c) 2024 Vitor Pamplona
31 | *
32 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
33 | * this software and associated documentation files (the "Software"), to deal in
34 | * the Software without restriction, including without limitation the rights to use,
35 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
36 | * Software, and to permit persons to whom the Software is furnished to do so,
37 | * subject to the following conditions:
38 | *
39 | * The above copyright notice and this permission notice shall be included in all
40 | * copies or substantial portions of the Software.
41 | *
42 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
43 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
44 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
45 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
46 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
47 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
48 | */
49 |
50 | data class NRelay(
51 | val relay: List,
52 | ) : Entity {
53 | companion object {
54 | fun parse(bytes: ByteArray): NRelay? {
55 | if (bytes.isEmpty()) return null
56 |
57 | val relayUrl = Tlv.parse(bytes).asStringList(TlvTypes.SPECIAL.id) ?: return null
58 |
59 | return NRelay(relayUrl)
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/net/NostrUtils.kt:
--------------------------------------------------------------------------------
1 | package rhodium.net
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.plugins.*
5 | import io.ktor.client.request.*
6 | import io.ktor.client.statement.*
7 | import kotlinx.serialization.json.jsonArray
8 | import kotlinx.serialization.json.jsonObject
9 | import kotlinx.serialization.json.jsonPrimitive
10 | import rhodium.nostr.Nip05ValidationError
11 | import rhodium.nostr.arraySerializer
12 | import rhodium.nostr.eventMapper
13 |
14 | object NostrUtils {
15 |
16 | suspend fun getProfileInfoFromAddress(
17 | nip05: String,
18 | client: HttpClient = httpClient()
19 | ): Array {
20 | val nameWithDomain = nip05.split("@")
21 | if (nameWithDomain.size > 2 || nameWithDomain.isEmpty()) {
22 | throw Nip05ValidationError("Likely a malformed address.")
23 | }
24 | else if (nameWithDomain.size == 1) {
25 | val domain = nameWithDomain[0]
26 | if (!UrlUtil.isValidUrl(domain)) throw Nip05ValidationError("Invalid identifier.")
27 | val urlToUse = "https://${domain}/.well-known/nostr.json?name=_"
28 | return fetchDetails(urlToUse, "_", client)
29 |
30 | }
31 | else {
32 | val finalUrl = "https://${nameWithDomain[1]}/.well-known/nostr.json?name=${nameWithDomain[0]}"
33 | return fetchDetails(finalUrl, nameWithDomain[0], client)
34 | }
35 | }
36 |
37 | private suspend fun fetchDetails(composedUrl: String, userName: String, client: HttpClient): Array {
38 | val obtainedResponse = client.config { followRedirects = false }.get(urlString = composedUrl)
39 |
40 | if (obtainedResponse.status.value in 200..299){
41 | val responseData = obtainedResponse.bodyAsText()
42 |
43 | return parseResponseData(responseData, userName)
44 | }
45 | else throw ResponseException(obtainedResponse, obtainedResponse.status.description)
46 | }
47 |
48 | private fun parseResponseData(responseData: String, userName: String): Array {
49 | val motherObject = eventMapper.parseToJsonElement(responseData).jsonObject
50 | val namesChild = motherObject["names"]?.jsonObject
51 | val profile = namesChild?.get(userName)?.jsonPrimitive?.content
52 | if (profile == null) {
53 | throw Nip05ValidationError("Could not find a corresponding pubkey for this address.")
54 | }
55 | else {
56 | val relaysChild = motherObject["relays"]?.jsonObject
57 | val userRelays = relaysChild?.get(profile)?.jsonArray
58 | if (userRelays == null) {
59 | return arrayOf(profile)
60 | }
61 | else {
62 | val relayList = eventMapper.decodeFromJsonElement(arraySerializer, userRelays)
63 | return arrayOf(profile, *relayList)
64 | }
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonTest/kotlin/rhodium/nostr/relay/RelayMessageTests.kt:
--------------------------------------------------------------------------------
1 | package rhodium.nostr.relay
2 |
3 | import kotlinx.serialization.encodeToString
4 | import kotlinx.serialization.json.Json
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 |
8 | class RelayMessageTests {
9 | private val relayMessageMapper = Json
10 |
11 | @Test
12 | fun `the relay message is correctly parsed to a notice`() {
13 | val relayNoticeJson = """["NOTICE", "You are not allowed to publish to this relay"]"""
14 | val relayNotice = relayMessageMapper.decodeFromString(relayNoticeJson)
15 | val correctRelayNotice = RelayNotice(
16 | "NOTICE",
17 | "You are not allowed to publish to this relay"
18 | )
19 | val correctRelayNoticeJson = relayMessageMapper.encodeToString(correctRelayNotice)
20 | println(correctRelayNoticeJson)
21 | println("Relay notice: $relayNotice")
22 | println("Correct relay notice: $correctRelayNotice")
23 | assertEquals(correctRelayNotice, relayNotice, "Relay notice conversion failed.")
24 | }
25 |
26 | @Test
27 | fun `the relay message is correctly parsed to a relay message event`() {
28 | val relayMessageJson =
29 | """["EVENT","mySub",{"id":"f0d7e34a1f531c04fee5846ee85ae564e9e0ed389e82fd79c58f2bedca19b0e4","pubkey":"056ccc33d638633ecc60ee28db5f226ad3acfdfe69a73aa9670cc8beb0d9dc74","created_at":1640340342,"kind":0,"tags":[],"content":"{\"name\":\"Colby\",\"picture\":\"https://a57.foxnews.com/static.foxbusiness.com/foxbusiness.com/content/uploads/2020/12/0/0/Bitcoin-Gold.jpg?ve=1&tl=1\",\"about\":\"Testing\"}","sig":"f0fa211543b32adf6892ee5f18f85e70946dc1579fd390b04d12579d919d9afbb5195395e8b3ccb3c2d03fa21184c0935c322151b584152cbc3c89b2048f8442"}]"""
30 | val relayMessage = relayMessageMapper.decodeFromString(relayMessageJson)
31 | val correctRelayMessage = RelayEventMessage(
32 | "EVENT",
33 | "mySub",
34 | "{\"id\":\"f0d7e34a1f531c04fee5846ee85ae564e9e0ed389e82fd79c58f2bedca19b0e4\",\"pubkey\":\"056ccc33d638633ecc60ee28db5f226ad3acfdfe69a73aa9670cc8beb0d9dc74\",\"created_at\":1640340342,\"kind\":0,\"tags\":[],\"content\":\"{\\\"name\\\":\\\"Colby\\\",\\\"picture\\\":\\\"https://a57.foxnews.com/static.foxbusiness.com/foxbusiness.com/content/uploads/2020/12/0/0/Bitcoin-Gold.jpg?ve=1&tl=1\\\",\\\"about\\\":\\\"Testing\\\"}\",\"sig\":\"f0fa211543b32adf6892ee5f18f85e70946dc1579fd390b04d12579d919d9afbb5195395e8b3ccb3c2d03fa21184c0935c322151b584152cbc3c89b2048f8442\"}"
35 | )
36 | val correctEventMessageJson = relayMessageMapper.encodeToString(correctRelayMessage)
37 | println(correctEventMessageJson)
38 | println("Relay Message: $relayMessage")
39 | println("Correct relay message: $correctRelayMessage")
40 | assertEquals(correctRelayMessage, relayMessage, "Relay message conversion failed.")
41 | }
42 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/rhodium-core/src/commonTest/kotlin/rhodium/nostr/EventTests.kt:
--------------------------------------------------------------------------------
1 | package rhodium.nostr
2 |
3 | import kotlinx.serialization.json.Json
4 | import kotlin.test.Test
5 | import kotlin.test.assertEquals
6 |
7 | class EventTests {
8 | private val testEventMapper = Json
9 | private val testSecKeyHex = "6ba903b7888191180a0959a6d286b9d0719d33a47395c519ba107470412d2069"
10 | private val testPubKeyHex = "8565b1a5a63ae21689b80eadd46f6493a3ed393494bb19d0854823a757d8f35f"
11 |
12 | @Test
13 | fun `it generates the correct raw event json for obtaining the eventId`() {
14 | val someTags = listOf(
15 | Tag("p", "42365g3ghgf7gg15hj64jk")
16 | //Triple("#e", "546454ghgfnfg56456fgngg", "wss://relayer.fiatjaf.com")
17 | )
18 | println("Test 1:")
19 | val rawEventData = listOf(
20 | testPubKeyHex,
21 | "1649108200", "1", "Testing some event"
22 | )
23 | val rawEventInJson = rawEventJson0(
24 | rawEventData[0], rawEventData[1].toLong(), rawEventData[2].toInt(),
25 | someTags, rawEventData[3]
26 | )
27 | val correctRawJson =
28 | "[0,\"8565b1a5a63ae21689b80eadd46f6493a3ed393494bb19d0854823a757d8f35f\",1649108200,1,[[\"p\",\"42365g3ghgf7gg15hj64jk\"]],\"Testing some event\"]"
29 | println(rawEventInJson)
30 | assertEquals(rawEventInJson, correctRawJson)
31 |
32 | }
33 |
34 | // @Test
35 | // fun `it generates correct events with multiple tags`() {
36 | // assert(true)
37 | // }
38 |
39 | @Test
40 | fun `testing the equivalence of event ids during event generation`(){
41 | val profileEvent = Events.MetadataEvent(testSecKeyHex, testPubKeyHex, profile = "Name.", timeStamp = 1640839235)
42 | val correspondingEvent = Events.generateEvent(EventKind.METADATA.kind, emptyList(),
43 | "Name.", testSecKeyHex, testPubKeyHex, timeStamp = 1640839235)
44 | println("Profile Ev: $profileEvent")
45 | println("Corr. Ev: $correspondingEvent")
46 | assertEquals(profileEvent.id, correspondingEvent.id)
47 | }
48 |
49 | @Test
50 | fun `the relay auth event is generated correctly`(){
51 | val testAuthRelay = "host.relay.local"
52 | val testChallenge = "i6pore5YD2DPHOUFtnqnNclXZlZrtIfEYFUCpoOSj58YQWJd2N27pc1BaMpDqpj8"
53 | val authEvent = Events.AuthEvent(testSecKeyHex, testPubKeyHex, testAuthRelay, testChallenge, 1725339072)
54 | val authEventJson = """{"id":"3d96a19522e598a9461d0e211fb893b444ddb731d7db2a695a4dd049e0c08318","pubkey":"8565b1a5a63ae21689b80eadd46f6493a3ed393494bb19d0854823a757d8f35f","created_at":1725339072,"kind":22242,"tags":[["relay","host.relay.local"],["challenge","i6pore5YD2DPHOUFtnqnNclXZlZrtIfEYFUCpoOSj58YQWJd2N27pc1BaMpDqpj8"]],"content":"","sig":"fd3c40858109743b900f24daaf6f000112bc54d13ed9ba17ba3b9b21f566950ba2b5bce6a3a39b6034f8c670444ebafdcd3504cfd89602dd9b105fbf03bc7be1"}"""
55 | val regeneratedAuthEvent = deserializedEvent(authEventJson)
56 |
57 | assertEquals(authEvent.tags, regeneratedAuthEvent.tags)
58 | }
59 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/nostr/relay/info/RetentionPolicy.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2025 KotlinGeekDev
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | *
24 | */
25 |
26 | package rhodium.nostr.relay.info
27 |
28 | import kotlinx.serialization.SerialName
29 | import kotlinx.serialization.Serializable
30 |
31 | /**
32 | * Represents a relay's data retention policies, per NIP-11,
33 | * on [event retention](https://github.com/nostr-protocol/nips/blob/master/11.md#event-retention).
34 | *
35 | * @property retainedKinds - A set of event kinds specified by the relay.
36 | * Events of these kinds are the ones kept by the relay.
37 | * @property retentionTime - The amount of time for which a set of events(specified by `retainedKinds` above)
38 | * will be kept by the relay, measured in seconds.
39 | * A value of zero(`0`) indicates that the relay won't store that event kind, and a `null` value indicates that
40 | * the event set will be stored forever.
41 | * @property retainedEventCount - The number of events for a particular
42 | * kind(or all kinds mentioned in `retainedKinds` above) that will be stored by the relay.
43 | */
44 | @Serializable
45 | data class RetentionPolicy(
46 | @SerialName("kinds") val retainedKinds: IntArray = emptyArray().toIntArray(),
47 | @SerialName("time") val retentionTime: Long? = 0L,
48 | @SerialName("count") val retainedEventCount: Int = 0
49 | ) {
50 | override fun equals(other: Any?): Boolean {
51 | if (this === other) return true
52 | if (other == null || this::class != other::class) return false
53 |
54 | other as RetentionPolicy
55 |
56 | if (retentionTime != other.retentionTime) return false
57 | if (retainedEventCount != other.retainedEventCount) return false
58 | if (!retainedKinds.contentEquals(other.retainedKinds)) return false
59 |
60 | return true
61 | }
62 |
63 | override fun hashCode(): Int {
64 | var result = retentionTime?.hashCode() ?: 0
65 | result = 31 * result + retainedEventCount
66 | result = 31 * result + retainedKinds.contentHashCode()
67 | return result
68 | }
69 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/CryptoUtils.kt:
--------------------------------------------------------------------------------
1 | //@file:JvmName("CryptoUtils")
2 |
3 | package rhodium.crypto
4 |
5 | import dev.whyoleg.cryptography.algorithms.SHA256
6 | import fr.acinq.secp256k1.Hex
7 | import fr.acinq.secp256k1.Secp256k1
8 | import fr.acinq.secp256k1.Secp256k1Exception
9 |
10 | //Class containing all the cryptographic helpers for Nostr
11 | object CryptoUtils {
12 |
13 | fun generatePrivateKey(): ByteArray {
14 | val secretKey = ByteArray(32)
15 | val pseudoRandomBytes = SecureRandom()
16 | pseudoRandomBytes.nextBytes(secretKey)
17 | return secretKey
18 | }
19 |
20 | /**
21 | * Generates(creates) a 32-byte public key from the provided private key.
22 | * @param privateKey the 32-byte private key, provided as a byte
23 | * array.
24 | *
25 | * @return the public key, as a byte array.
26 | */
27 | fun getPublicKey(privateKey: ByteArray): ByteArray {
28 | if (!Secp256k1.secKeyVerify(privateKey)) throw Exception("Invalid private key!")
29 | val pubKey = Secp256k1.pubkeyCreate(privateKey).drop(1).take(32).toByteArray()
30 | //context.cleanup()
31 | return pubKey
32 | }
33 |
34 | /**
35 | * Function that returns the hash of content
36 | * @param content the content to be hashed
37 | * @return the content hash, as a byte array.
38 | */
39 | //TODO: Should the function return a string or a byte array?
40 | fun contentHash(content: String): ByteArray {
41 | return getCryptoProvider().get(SHA256)
42 | .hasher()
43 | .hashBlocking(content.encodeToByteArray())
44 |
45 | }
46 |
47 | /**
48 | * The function signs the content provided to it, and
49 | * returns the 64-byte schnorr signature of the content.
50 | * @param privateKey the private key used for signing, provided
51 | * as a byte array.
52 | * @param content the content to be signed, provided as a
53 | * byte array.
54 | *
55 | * @return the 64-byte signature, as a byte array.
56 | */
57 | @Throws(Error::class)
58 | fun signContent(privateKey: ByteArray, content: ByteArray): ByteArray {
59 | val freshRandomBytes = ByteArray(32)
60 | SecureRandom().nextBytes(freshRandomBytes)
61 | val contentSignature = Secp256k1.signSchnorr(content, privateKey, freshRandomBytes)
62 | return contentSignature
63 | }
64 |
65 | /**
66 | * The function verifies the provided 64-byte signature.
67 | * @param signature the signature to provide, as a byte array.
68 | * @param publicKey the 32-byte public key to provide, as a byte array.
69 | * @param content the signed content to provide, as a byte array.
70 | *
71 | * @return the validity of the signature, as a boolean.
72 | */
73 | @Throws(Secp256k1Exception::class)
74 | fun verifyContentSignature(
75 | signature: ByteArray,
76 | publicKey: ByteArray,
77 | content: ByteArray
78 | ): Boolean {
79 | val verificationStatus = Secp256k1.verifySchnorr(signature, content, publicKey)
80 |
81 | return verificationStatus
82 | }
83 |
84 | }
85 |
86 | fun ByteArray.toHexString() = Hex.encode(this)
87 | fun String.toBytes() = Hex.decode(this)
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/tlv/entity/EntityUtils.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 KotlinGeekDev
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | *
22 | */
23 |
24 | package rhodium.crypto.tlv.entity
25 |
26 | /**
27 | * Copyright (c) 2024 Vitor Pamplona
28 | *
29 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
30 | * this software and associated documentation files (the "Software"), to deal in
31 | * the Software without restriction, including without limitation the rights to use,
32 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
33 | * Software, and to permit persons to whom the Software is furnished to do so,
34 | * subject to the following conditions:
35 | *
36 | * The above copyright notice and this permission notice shall be included in all
37 | * copies or substantial portions of the Software.
38 | *
39 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
41 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
42 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
43 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
44 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45 | */
46 |
47 | import rhodium.crypto.Bech32
48 |
49 |
50 | fun ByteArray.toNsec() = Bech32.encodeBytes(hrp = "nsec", this, Bech32.Encoding.Bech32)
51 |
52 | fun ByteArray.toNpub() = Bech32.encodeBytes(hrp = "npub", this, Bech32.Encoding.Bech32)
53 |
54 | @Deprecated("Prefer nevent1 instead")
55 | fun ByteArray.toNote() = Bech32.encodeBytes(hrp = "note", this, Bech32.Encoding.Bech32)
56 |
57 | fun ByteArray.toNEvent() = Bech32.encodeBytes(hrp = "nevent", this, Bech32.Encoding.Bech32)
58 |
59 | fun ByteArray.toNProfile() = Bech32.encodeBytes(hrp = "nprofile", this, Bech32.Encoding.Bech32)
60 |
61 | fun ByteArray.toNAddress() = Bech32.encodeBytes(hrp = "naddr", this, Bech32.Encoding.Bech32)
62 |
63 | fun ByteArray.toLnUrl() = Bech32.encodeBytes(hrp = "lnurl", this, Bech32.Encoding.Bech32)
64 |
65 | fun ByteArray.toNEmbed() = Bech32.encodeBytes(hrp = "nembed", this, Bech32.Encoding.Bech32)
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/nostr/Tag.kt:
--------------------------------------------------------------------------------
1 | //@file:JvmName("Tag")
2 | package rhodium.nostr
3 |
4 | import kotlinx.serialization.ExperimentalSerializationApi
5 | import kotlinx.serialization.KSerializer
6 | import kotlinx.serialization.Serializable
7 | import kotlinx.serialization.descriptors.SerialDescriptor
8 | import kotlinx.serialization.encoding.Decoder
9 | import kotlinx.serialization.encoding.Encoder
10 |
11 | /**
12 | * The model representing the event tag. The event tag carries
13 | * information such as what identifies the tag('p', 'e', etc.), the tag's
14 | * content, or description(a referenced pubkey, event, etc.),
15 | * a recommended relay url(optional), which you can add to the list
16 | * of relays you already have, and a petname or username(optional),
17 | * when a tag contains an identity's alias(or username).
18 | *
19 | * @param identifier The tag key, as a string
20 | * @param description The tag value, as a string
21 | * @param content (optional) A custom field with no particular meaning(generally used for relay recommendation), as a string
22 | */
23 |
24 |
25 | @Serializable(with = Tag.TagSerializer::class)
26 | data class Tag(
27 | val identifier: String, val description: String,
28 | val content: String? = null,
29 | val customContent: String? = null
30 | ) {
31 |
32 | @OptIn(ExperimentalSerializationApi::class)
33 | internal class TagSerializer : KSerializer {
34 | private val builtinSerializer = arraySerializer
35 | override val descriptor: SerialDescriptor = SerialDescriptor("Tag", builtinSerializer.descriptor)
36 | override fun serialize(encoder: Encoder, value: Tag) {
37 | val arrayOfValues = with(value){
38 | buildList {
39 | add(identifier)
40 | add(description)
41 | if (content != null) add(content)
42 | if (customContent != null) add(customContent)
43 | }.toTypedArray()
44 | }
45 | encoder.encodeSerializableValue(builtinSerializer, arrayOfValues)
46 | }
47 |
48 | override fun deserialize(decoder: Decoder): Tag {
49 | val array = decoder.decodeSerializableValue(builtinSerializer)
50 | val arraySize = array.size
51 | return when {
52 | arraySize > 4 || arraySize < 2 -> throw Exception("Incorrect tag format.")
53 | arraySize == 4 -> Tag(array[0], array[1], array[2], array[3])
54 | arraySize == 3 -> Tag(array[0], array[1], array[2])
55 | else -> Tag(array[0], array[1])
56 | }
57 | }
58 | }
59 |
60 | }
61 |
62 | //----Kept for legacy purposes, or when serialization above does not work----
63 | ///**
64 | // * Transforms a list of tags to a list of string arrays.
65 | // * This function is necessary in order for the tag structure to
66 | // * be serialized correctly.
67 | // *
68 | // * @return An array of string arrays
69 | // */
70 | //fun List.toStringList(): List> {
71 | // val tagStringList: MutableList> = mutableListOf()
72 | // this.forEach { tag ->
73 | // val elementList: List = if (tag.customContent != null){
74 | // listOf(tag.identifier, tag.content, tag.customContent)
75 | // } else {
76 | // listOf(tag.identifier, tag.content)
77 | // }
78 | // tagStringList.add(elementList)
79 | // }
80 | //
81 | // return tagStringList.toList()
82 | //}
83 |
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/tlv/entity/NProfile.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 KotlinGeekDev
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | *
22 | */
23 |
24 | package rhodium.crypto.tlv.entity
25 |
26 | /**
27 | * Copyright (c) 2024 Vitor Pamplona
28 | *
29 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
30 | * this software and associated documentation files (the "Software"), to deal in
31 | * the Software without restriction, including without limitation the rights to use,
32 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
33 | * Software, and to permit persons to whom the Software is furnished to do so,
34 | * subject to the following conditions:
35 | *
36 | * The above copyright notice and this permission notice shall be included in all
37 | * copies or substantial portions of the Software.
38 | *
39 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
41 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
42 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
43 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
44 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45 | */
46 |
47 | import rhodium.crypto.tlv.Tlv
48 | import rhodium.crypto.tlv.TlvBuilder
49 | import rhodium.crypto.tlv.TlvTypes
50 |
51 | data class NProfile(
52 | val hex: String,
53 | val relay: List,
54 | ) : Entity {
55 | companion object {
56 | fun parse(bytes: ByteArray): NProfile? {
57 | if (bytes.isEmpty()) return null
58 |
59 | val tlv = Tlv.parse(bytes)
60 |
61 | val hex = tlv.firstAsHex(TlvTypes.SPECIAL.id) ?: return null
62 | val relay = tlv.asStringList(TlvTypes.RELAY.id) ?: emptyList()
63 |
64 | if (hex.isBlank()) return null
65 |
66 | return NProfile(hex, relay)
67 | }
68 |
69 | fun create(
70 | authorPubKeyHex: String,
71 | relays: List,
72 | ): String =
73 | TlvBuilder()
74 | .apply {
75 | addHex(TlvTypes.SPECIAL.id, authorPubKeyHex)
76 | relays.forEach {
77 | addStringIfNotNull(TlvTypes.RELAY.id, it)
78 | }
79 | }.build()
80 | .toNProfile()
81 | }
82 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/nostr/relay/RelayPool.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2025 KotlinGeekDev
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | *
24 | */
25 |
26 | package rhodium.nostr.relay
27 |
28 | import rhodium.nostr.RelayError
29 | import kotlin.jvm.JvmStatic
30 |
31 | /**
32 | * Handles the relays used by the `NostrService`.
33 | * For now, it is very simple. It just handles addition,
34 | * removal, and resetting of the relay pool.
35 | *
36 | * You can specify a custom list of relays by using the `Relay(...)` primary constructor,
37 | * or using [RelayPool.fromUrls].
38 | */
39 | class RelayPool {
40 |
41 | private val relayList: MutableList = mutableListOf()
42 | constructor(){
43 | getDefaultRelays().forEach {
44 | relayList.add(it)
45 | }
46 | }
47 |
48 | constructor(relays: List) : this() {
49 | relays.forEach { relayList.add(it) }
50 | }
51 |
52 | fun getRelays() = relayList.toList()
53 |
54 | fun addRelay(relay: Relay) {
55 | if (relayList.add(relay))
56 | return
57 | else throw RelayError("The relay ${relay.relayURI} could not be added.")
58 | }
59 |
60 | fun addRelays(vararg relayUrls: String){
61 | relayUrls.forEach { url ->
62 | addRelay(Relay(url))
63 | }
64 | }
65 |
66 | fun addRelays(relays: Collection) {
67 | relays.forEach { relayList.add(it) }
68 | }
69 |
70 | fun addRelayList(listOfRelays: Collection) {
71 | val relayRefs = listOfRelays.map { Relay(it) }
72 | addRelays(relayRefs)
73 | }
74 |
75 | fun removeRelay(relay: Relay) {
76 | relayList.remove(relay)
77 | }
78 |
79 | fun clearPool() {
80 | relayList.clear()
81 | }
82 |
83 | companion object {
84 |
85 | fun fromUrls(vararg relayUris: String): RelayPool {
86 | val relayList = relayUris.map { Relay(it) }
87 | return RelayPool(relayList)
88 | }
89 |
90 | fun fromUrls(urlList: Collection): RelayPool {
91 | val relayList = urlList.map { Relay(it) }
92 | return RelayPool(relayList)
93 | }
94 |
95 | @JvmStatic
96 | fun getDefaultRelays(): List = listOf(
97 | Relay("wss://nostr-pub.wellorder.net"),
98 | Relay("wss://relay.damus.io"),
99 | Relay("wss://relay.nostr.wirednet.jp"),
100 | Relay("wss://relay.nostr.band"),
101 | )
102 | }
103 |
104 | }
105 |
106 |
107 |
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/nostr/NostrFilter.kt:
--------------------------------------------------------------------------------
1 | package rhodium.nostr
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | class NostrFilter private constructor(
8 | @SerialName("ids") private val listOfIds: List? = null,
9 | @SerialName("authors") private val authorsList: List? = null,
10 | @SerialName("kinds") private val listOfKinds: List,
11 | @SerialName("#e") private val eventIdList: List? = null,
12 | @SerialName("#p") private val pubkeyList: List? = null,
13 | @SerialName("#t") private val topicList: List? = null,
14 | private val since: Long? = null,
15 | private val until: Long? = null,
16 | private val search: String? = null,
17 | private val limit: Int = 1
18 | ) {
19 |
20 | override fun toString() = """
21 | Ids:$listOfIds
22 | Authors:$authorsList
23 | Kinds:$listOfKinds
24 | Tags
25 | Id:$eventIdList
26 | Pubkey:$pubkeyList
27 | Topic:$topicList
28 | Since:$since
29 | Until:$until
30 | Search:$search
31 | Limit:$limit
32 | """.trimIndent()
33 |
34 | companion object {
35 | fun newFilter() = Builder()
36 | }
37 |
38 | class Builder {
39 | private var listOfIds: List? = null
40 | private var authorsList: List? = null
41 | private var listOfKinds: List = emptyList()
42 | private var eventTagList: List? = null
43 | private var pubkeyTagList:List? = null
44 | private var topicList: List? = null
45 | private var since: Long? = null
46 | private var until: Long? = null
47 | private var search: String? = null
48 | private var limit: Int = 1
49 |
50 | fun idList(vararg iDList: String = emptyArray()) = apply {
51 | listOfIds = if (iDList.isEmpty()) null else iDList.toList()
52 | }
53 |
54 | fun authors(vararg authorList: String = emptyArray()) = apply {
55 | authorsList = if (authorList.isEmpty()) null else authorList.toList()
56 | }
57 |
58 | fun kinds(vararg kindList: Int) = apply {
59 | listOfKinds = kindList.toList()
60 | }
61 |
62 | fun eventTagList(vararg listOfEventTags: String = emptyArray()) = apply {
63 | eventTagList = if (listOfEventTags.isEmpty()) null else listOfEventTags.toList()
64 | }
65 |
66 | fun pubkeyTagList(vararg pubkeyList: String = emptyArray()) = apply {
67 | pubkeyTagList = if (pubkeyList.isEmpty()) null else pubkeyList.toList()
68 | }
69 |
70 | fun topics(vararg listOfTopics: String = emptyArray()) = apply {
71 | topicList = if (listOfTopics.isEmpty()) null else listOfTopics.toList()
72 | }
73 |
74 | fun since(timeStamp: Long? = null) = apply {
75 | since = timeStamp
76 | }
77 |
78 | fun until(timeStamp: Long? = null) = apply {
79 | until = timeStamp
80 | }
81 |
82 | fun search(searchString: String? = null) = apply {
83 | search = searchString
84 | }
85 |
86 | fun limit(receivingEventLimit: Int) = apply {
87 | limit = receivingEventLimit
88 | }
89 |
90 | fun build() = NostrFilter(
91 | listOfIds = listOfIds,
92 | authorsList = authorsList,
93 | listOfKinds = listOfKinds,
94 | eventIdList = eventTagList,
95 | pubkeyList = pubkeyTagList,
96 | topicList = topicList,
97 | since = since,
98 | until = until,
99 | search = search,
100 | limit = limit
101 | )
102 | }
103 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/tlv/TlvBuilder.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 KotlinGeekDev
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | *
22 | */
23 |
24 | package rhodium.crypto.tlv
25 |
26 | /**
27 | * Copyright (c) 2024 Vitor Pamplona
28 | *
29 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
30 | * this software and associated documentation files (the "Software"), to deal in
31 | * the Software without restriction, including without limitation the rights to use,
32 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
33 | * Software, and to permit persons to whom the Software is furnished to do so,
34 | * subject to the following conditions:
35 | *
36 | * The above copyright notice and this permission notice shall be included in all
37 | * copies or substantial portions of the Software.
38 | *
39 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
41 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
42 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
43 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
44 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45 | */
46 |
47 | import kotlinx.io.Buffer
48 | import kotlinx.io.readByteArray
49 | import rhodium.crypto.toBytes
50 |
51 | class TlvBuilder {
52 | val outputStream = Buffer()
53 |
54 | private fun add(
55 | type: Byte,
56 | byteArray: ByteArray,
57 | ) {
58 | outputStream.write(byteArrayOf(type, byteArray.size.toByte()))
59 | outputStream.write(byteArray)
60 | }
61 |
62 | fun addString(
63 | type: Byte,
64 | string: String,
65 | ) = add(type, string.encodeToByteArray())
66 |
67 | fun addHex(
68 | type: Byte,
69 | key: String,
70 | ) = add(type, key.toBytes())
71 |
72 | fun addInt(
73 | type: Byte,
74 | data: Int,
75 | ) = add(type, data.to32BitByteArray())
76 |
77 | fun addStringIfNotNull(
78 | type: Byte,
79 | data: String?,
80 | ) = data?.let { addString(type, it) }
81 |
82 | fun addHexIfNotNull(
83 | type: Byte,
84 | data: String?,
85 | ) = data?.let { addHex(type, it) }
86 |
87 | fun addIntIfNotNull(
88 | type: Byte,
89 | data: Int?,
90 | ) = data?.let { addInt(type, it) }
91 |
92 | fun build(): ByteArray = outputStream.readByteArray()
93 | }
94 |
95 | fun Int.to32BitByteArray(): ByteArray {
96 | val bytes = ByteArray(4)
97 | (0..3).forEach { bytes[3 - it] = ((this ushr (8 * it)) and 0xFFFF).toByte() }
98 | return bytes
99 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/tlv/entity/NEvent.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 KotlinGeekDev
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | *
22 | */
23 |
24 | package rhodium.crypto.tlv.entity
25 |
26 | /**
27 | * Copyright (c) 2024 Vitor Pamplona
28 | *
29 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
30 | * this software and associated documentation files (the "Software"), to deal in
31 | * the Software without restriction, including without limitation the rights to use,
32 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
33 | * Software, and to permit persons to whom the Software is furnished to do so,
34 | * subject to the following conditions:
35 | *
36 | * The above copyright notice and this permission notice shall be included in all
37 | * copies or substantial portions of the Software.
38 | *
39 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
41 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
42 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
43 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
44 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45 | */
46 |
47 | import rhodium.crypto.tlv.Tlv
48 | import rhodium.crypto.tlv.TlvBuilder
49 | import rhodium.crypto.tlv.TlvTypes
50 |
51 | data class NEvent(
52 | val hex: String,
53 | val relay: List,
54 | val author: String?,
55 | val kind: Int?,
56 | ) : Entity {
57 | companion object {
58 | fun parse(bytes: ByteArray): NEvent? {
59 | if (bytes.isEmpty()) return null
60 |
61 | val tlv = Tlv.parse(bytes)
62 |
63 | val hex = tlv.firstAsHex(TlvTypes.SPECIAL.id) ?: return null
64 | val relay = tlv.asStringList(TlvTypes.RELAY.id) ?: emptyList()
65 | val author = tlv.firstAsHex(TlvTypes.AUTHOR.id)
66 | val kind = tlv.firstAsInt(TlvTypes.KIND.id)
67 |
68 | if (hex.isBlank()) return null
69 |
70 | return NEvent(hex, relay, author, kind)
71 | }
72 |
73 | fun create(
74 | idHex: String,
75 | author: String?,
76 | kind: Int?,
77 | relay: String?,
78 | ): String =
79 | TlvBuilder()
80 | .apply {
81 | addHex(TlvTypes.SPECIAL.id, idHex)
82 | addStringIfNotNull(TlvTypes.RELAY.id, relay)
83 | addHexIfNotNull(TlvTypes.AUTHOR.id, author)
84 | addIntIfNotNull(TlvTypes.KIND.id, kind)
85 | }.build()
86 | .toNEvent()
87 | }
88 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/nostr/EventExt.kt:
--------------------------------------------------------------------------------
1 | package rhodium.nostr
2 |
3 | import fr.acinq.secp256k1.Hex
4 | import kotlinx.serialization.ExperimentalSerializationApi
5 | import kotlinx.serialization.KSerializer
6 | import kotlinx.serialization.builtins.ArraySerializer
7 | import kotlinx.serialization.builtins.serializer
8 | import kotlinx.serialization.descriptors.SerialDescriptor
9 | import kotlinx.serialization.encodeToString
10 | import kotlinx.serialization.encoding.Decoder
11 | import kotlinx.serialization.encoding.Encoder
12 | import kotlinx.serialization.json.Json
13 | import rhodium.crypto.CryptoUtils
14 |
15 | internal val eventMapper = Json { ignoreUnknownKeys = true }
16 | @OptIn(ExperimentalSerializationApi::class)
17 | internal val arraySerializer: KSerializer> = ArraySerializer(String.serializer())
18 |
19 | internal class StringArraySerializer : KSerializer> {
20 | private val builtinSerializer = arraySerializer
21 | override val descriptor: SerialDescriptor
22 | get() = builtinSerializer.descriptor
23 |
24 | override fun deserialize(decoder: Decoder): Array {
25 | return decoder.decodeSerializableValue(builtinSerializer)
26 | }
27 |
28 | override fun serialize(encoder: Encoder, value: Array) {
29 | encoder.encodeSerializableValue(builtinSerializer, value)
30 | }
31 |
32 | }
33 |
34 | /**
35 | * Keeping this function below for legacy purposes.
36 | */
37 | //fun Event.toJson(): String {
38 | //
39 | // val listOfTagsJson = if (tags.isEmpty()) "[]"
40 | // else buildString {
41 | // append("[")
42 | // tags.forEach { tagJson ->
43 | // if (tagJson.size == 3) {
44 | // val tag = Tag(tagJson[0], tagJson[1], tagJson[2])
45 | // append(
46 | // "[\"${tag.identifier}\",\"${tag.description}\"${
47 | // if (tag.recommendedRelayUrl.isNullOrEmpty()) ""
48 | // else ",\"${tag.recommendedRelayUrl}\""
49 | // }]"
50 | // )
51 | // append(",")
52 | // }
53 | //
54 | // }
55 | // deleteAt(lastIndexOf(","))
56 | // append("]")
57 | // }
58 | //
59 | // val event = buildString {
60 | // append("{")
61 | // append("\"id\":\"$id\",")
62 | // append("\"pubkey\":\"$pubkey\",")
63 | // append("\"created_at\":$creationDate,")
64 | // append("\"kind\":$eventKind,")
65 | // append("\"tags\":$listOfTagsJson,")
66 | // append("\"content\":\"$content\",")
67 | // append("\"sig\":\"$eventSignature\"")
68 | // append("}")
69 | // }
70 | //
71 | // return event
72 | //}
73 |
74 | fun Event.isValid(): Boolean {
75 | val eventId = getEventId(
76 | this.pubkey, this.creationDate, this.eventKind,
77 | this.tags, this.content
78 | )
79 | if (eventId != this.id) {
80 | println("The event id is invalid.")
81 | return false
82 | }
83 | val signatureValidity = CryptoUtils.verifyContentSignature(
84 | Hex.decode(this.eventSignature),
85 | Hex.decode(this.pubkey),
86 | Hex.decode(eventId)
87 | )
88 | if (!signatureValidity) {
89 | println("The event signature is invalid.\n Please check the pubkey, or content.")
90 | return false
91 | }
92 | return true
93 | }
94 |
95 | fun Event.serialize(): String {
96 | if (!this.isValid()) throw EventValidationError("Generated event is not valid")
97 | return eventMapper.encodeToString(this)
98 | }
99 |
100 | public fun deserializedEvent(eventJson: String): Event {
101 | val deserializedEvent = eventMapper.decodeFromString(eventJson)
102 | if (!deserializedEvent.isValid()) throw EventValidationError("The event is invalid. \n Event: $deserializedEvent")
103 | return deserializedEvent
104 | }
105 |
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/nostr/relay/info/RelayLimits.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2025 KotlinGeekDev
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | *
24 | */
25 |
26 | package rhodium.nostr.relay.info
27 |
28 | import kotlinx.serialization.SerialName
29 | import kotlinx.serialization.Serializable
30 |
31 | /**
32 | * Represents a relay's advertised limits.
33 | * For more, see [NIP-11](https://github.com/nostr-protocol/nips/blob/master/11.md).
34 | * All properties here are *optional*.
35 | *
36 | * @property maxMessageLength - The maximum length of the event to be published, in bytes.
37 | * @property maxSubscriptions - The maximum number of active subscriptions on a single connection to the relay.
38 | * @property maxLimit - The maximum limit for subscription filters sent to this relay.
39 | * @property maxSubscriptionIdLength - The maximum length of the subscription id string.
40 | * @property maxEventTagNumber - The maximum number of tags allowed in an event published to this relay.
41 | * @property maxContentLength - The maximum number of characters in the content field of an event to be published
42 | * to this relay.
43 | * @property minPowDifficulty - The minimum amount of PoW difficulty needed for an event to be published to this
44 | * relay.
45 | * @property isAuthRequired - Determines if the relay has placed limits on publishing events/sending requests.
46 | * @property creationDateLowerLimit - Determines the 'lowest date', or date furthest back in time,
47 | * that an event being published(or request being made) can reach.
48 | * @property creationDateUpperLimit - Determines the 'highest date', or date furthest into the future, that
49 | * an event being published(or request limit being made) to the relay can reach.
50 | * @property defaultLimit - The limit being applied by default, when a request is sent without any limits.
51 | */
52 | @Serializable
53 | data class RelayLimits(
54 | @SerialName("max_message_length") val maxMessageLength: Int? = null,
55 | @SerialName("max_subscriptions") val maxSubscriptions: Int? = null,
56 | @SerialName("max_limit") val maxLimit: Int? = null,
57 | @SerialName("max_subid_length") val maxSubscriptionIdLength: Int? = null,
58 | @SerialName("max_event_tags") val maxEventTagNumber: Int? = null,
59 | @SerialName("max_content_length") val maxContentLength: Int? = null,
60 | @SerialName("min_pow_difficulty") val minPowDifficulty: Int? = null,
61 | @SerialName("auth_required") val isAuthRequired: Boolean? = null,
62 | @SerialName("payment_required") val isPaymentRequired: Boolean? = null,
63 | @SerialName("restricted_writes") val writesAreRestricted: Boolean? = null,
64 | @SerialName("created_at_lower_limit") val creationDateLowerLimit: Long? = null,
65 | @SerialName("created_at_upper_limit") val creationDateUpperLimit: Long? = null,
66 | @SerialName("default_limit") val defaultLimit: Int? = null
67 | )
68 |
69 |
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/tlv/Tlv.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 KotlinGeekDev
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | *
22 | */
23 |
24 | package rhodium.crypto.tlv
25 |
26 | /**
27 | * Copyright (c) 2024 Vitor Pamplona
28 | *
29 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
30 | * this software and associated documentation files (the "Software"), to deal in
31 | * the Software without restriction, including without limitation the rights to use,
32 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
33 | * Software, and to permit persons to whom the Software is furnished to do so,
34 | * subject to the following conditions:
35 | *
36 | * The above copyright notice and this permission notice shall be included in all
37 | * copies or substantial portions of the Software.
38 | *
39 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
41 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
42 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
43 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
44 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45 | */
46 |
47 | import com.ditchoom.buffer.ByteOrder
48 | import com.ditchoom.buffer.PlatformBuffer
49 | import com.ditchoom.buffer.wrap
50 | import kotlinx.io.bytestring.ByteString
51 | import kotlinx.io.bytestring.decodeToString
52 | import rhodium.crypto.toHexString
53 |
54 | class Tlv(
55 | val data: Map>,
56 | ) {
57 | fun asInt(type: Byte) = data[type]?.mapNotNull { it.toInt32() }
58 |
59 | fun asHex(type: Byte) = data[type]?.map { it.toHexString() }
60 |
61 | fun asString(type: Byte) = data[type]?.map { ByteString(it).decodeToString() }
62 |
63 | fun firstAsInt(type: Byte) = data[type]?.firstOrNull()?.toInt32()
64 |
65 | fun firstAsHex(type: Byte) = data[type]?.firstOrNull()?.toHexString()
66 |
67 | fun firstAsString(type: Byte) = data[type]?.firstOrNull()?.run { ByteString(this).decodeToString() }
68 |
69 | fun asStringList(type: Byte) = data[type]?.map { ByteString(it).decodeToString() }
70 |
71 | companion object {
72 | fun parse(data: ByteArray): Tlv {
73 | val result = mutableMapOf>()
74 | var rest = data
75 | while (rest.isNotEmpty()) {
76 | val t = rest[0]
77 | val l = rest[1].toUByte().toInt()
78 | val v = rest.sliceArray(IntRange(2, (2 + l) - 1))
79 | rest = rest.sliceArray(IntRange(2 + l, rest.size - 1))
80 | if (v.size < l) continue
81 |
82 | if (!result.containsKey(t)) {
83 | result[t] = mutableListOf()
84 | }
85 | result[t]?.add(v)
86 | }
87 | return Tlv(result)
88 | }
89 | }
90 | }
91 |
92 | fun ByteArray.toInt32(): Int? {
93 | if (size != 4) return null
94 |
95 | return PlatformBuffer.wrap(this.copyOfRange(0, 4), ByteOrder.BIG_ENDIAN).readInt()
96 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/tlv/entity/NAddress.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 KotlinGeekDev
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | *
22 | */
23 |
24 | package rhodium.crypto.tlv.entity
25 |
26 | /**
27 | * Copyright (c) 2024 Vitor Pamplona
28 | *
29 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
30 | * this software and associated documentation files (the "Software"), to deal in
31 | * the Software without restriction, including without limitation the rights to use,
32 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
33 | * Software, and to permit persons to whom the Software is furnished to do so,
34 | * subject to the following conditions:
35 | *
36 | * The above copyright notice and this permission notice shall be included in all
37 | * copies or substantial portions of the Software.
38 | *
39 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
41 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
42 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
43 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
44 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45 | */
46 |
47 | import rhodium.crypto.bechToBytes
48 | import rhodium.crypto.tlv.Tlv
49 | import rhodium.crypto.tlv.TlvBuilder
50 | import rhodium.crypto.tlv.TlvTypes
51 | import rhodium.logging.serviceLogger
52 |
53 | data class NAddress(
54 | val kind: Int,
55 | val author: String,
56 | val dTag: String,
57 | val relay: List,
58 | ) : Entity {
59 | fun aTag(): String = "$kind:$author:$dTag"
60 |
61 | companion object {
62 | fun parse(naddr: String): NAddress? {
63 | try {
64 | val key = naddr.removePrefix("nostr:")
65 |
66 | if (key.startsWith("naddr")) {
67 | return parse(key.bechToBytes())
68 | }
69 | } catch (e: Throwable) {
70 | serviceLogger.w("Issue trying to Decode NIP19 $this: ${e.message}", e)
71 | // e.printStackTrace()
72 | }
73 |
74 | return null
75 | }
76 |
77 | fun parse(bytes: ByteArray): NAddress? {
78 |
79 | if (bytes.isEmpty()) return null
80 |
81 | val tlv = Tlv.parse(bytes)
82 |
83 | val d = tlv.firstAsString(TlvTypes.SPECIAL.id) ?: ""
84 | val relay = tlv.asStringList(TlvTypes.RELAY.id) ?: emptyList()
85 | val author = tlv.firstAsHex(TlvTypes.AUTHOR.id) ?: return null
86 | val kind = tlv.firstAsInt(TlvTypes.KIND.id) ?: return null
87 |
88 | return NAddress(kind, author, d, relay)
89 | }
90 |
91 | fun create(
92 | kind: Int,
93 | pubKeyHex: String,
94 | dTag: String,
95 | relay: String?,
96 | ): String =
97 | TlvBuilder()
98 | .apply {
99 | addString(TlvTypes.SPECIAL.id, dTag)
100 | addStringIfNotNull(TlvTypes.RELAY.id, relay)
101 | addHex(TlvTypes.AUTHOR.id, pubKeyHex)
102 | addInt(TlvTypes.KIND.id, kind)
103 | }.build()
104 | .toNAddress()
105 | }
106 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/nostr/relay/info/Payments.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2025 KotlinGeekDev
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | *
24 | */
25 |
26 | package rhodium.nostr.relay.info
27 |
28 | import kotlinx.serialization.SerialName
29 | import kotlinx.serialization.Serializable
30 |
31 | /**
32 | * Represents the different payment options given by the relay, provided the relay has paid features.
33 | * For more, see [Pay-to-Relay](https://github.com/nostr-protocol/nips/blob/master/11.md#pay-to-relay).
34 | * The payment options are divided into 3 nominal categories: admission, subscription, and publication.
35 | * Each category has a set of payment choices, each choice represented by a [PaymentInfo] object.
36 | *
37 | *@see PaymentInfo
38 | *
39 | * @property admissionFees The payment choices for *admission*(admission tiers), as an array.
40 | * @property subscriptionFees The payment choices for *subscription*(subscription tiers), as an array.
41 | * @property publicationFees The payment choices for *event publication*(publishing tiers), as an array.
42 | */
43 | @Serializable
44 | data class Payments(
45 | @SerialName("admission") val admissionFees: Array? = null,
46 | @SerialName("subscription") val subscriptionFees: Array? = null,
47 | @SerialName("publication") val publicationFees: Array? = null
48 | ) {
49 | override fun equals(other: Any?): Boolean {
50 | if (this === other) return true
51 | if (other == null || this::class != other::class) return false
52 |
53 | other as Payments
54 |
55 | if (!admissionFees.contentEquals(other.admissionFees)) return false
56 | if (!subscriptionFees.contentEquals(other.subscriptionFees)) return false
57 | if (!publicationFees.contentEquals(other.publicationFees)) return false
58 |
59 | return true
60 | }
61 |
62 | override fun hashCode(): Int {
63 | var result = admissionFees?.contentHashCode() ?: 0
64 | result = 31 * result + (subscriptionFees?.contentHashCode() ?: 0)
65 | result = 31 * result + (publicationFees?.contentHashCode() ?: 0)
66 | return result
67 | }
68 | }
69 |
70 | /**
71 | * Represents a payment choice, or tier, provided by the relay.
72 | *
73 | * @see Payments
74 | *
75 | * @property amount The amount to be paid for this tier.
76 | * @property unit The currency(or units of currency) for the amount.
77 | * @property durationInSeconds Specifies how long the usage of this tier will last.
78 | * @property eventKinds - The event kinds that will be retained,
79 | * or allowed for publication if paying for this tier.
80 | */
81 | @Serializable
82 | data class PaymentInfo(
83 | val amount: Long,
84 | val unit: String,
85 | @SerialName("period") val durationInSeconds: Long? = null,
86 | @SerialName("kinds") val eventKinds: IntArray? = null
87 | ) {
88 | override fun equals(other: Any?): Boolean {
89 | if (this === other) return true
90 | if (other == null || this::class != other::class) return false
91 |
92 | other as PaymentInfo
93 |
94 | if (amount != other.amount) return false
95 | if (durationInSeconds != other.durationInSeconds) return false
96 | if (unit != other.unit) return false
97 | if (!eventKinds.contentEquals(other.eventKinds)) return false
98 |
99 | return true
100 | }
101 |
102 | override fun hashCode(): Int {
103 | var result = amount.hashCode()
104 | result = 31 * result + (durationInSeconds?.hashCode() ?: 0)
105 | result = 31 * result + unit.hashCode()
106 | result = 31 * result + (eventKinds?.contentHashCode() ?: 0)
107 | return result
108 | }
109 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/nostr/Events.kt:
--------------------------------------------------------------------------------
1 | @file:JvmName("EventUtils")
2 | package rhodium.nostr
3 |
4 | import fr.acinq.secp256k1.Secp256k1
5 | import rhodium.crypto.CryptoUtils
6 | import rhodium.crypto.toBytes
7 | import rhodium.crypto.toHexString
8 | import rhodium.currentSystemTimestamp
9 | import kotlin.jvm.JvmName
10 | import kotlin.jvm.JvmStatic
11 |
12 | object Events {
13 |
14 | internal fun generateEvent(eventKind: Int,
15 | tags: List,
16 | content: String,
17 | privateKeyHex: String,
18 | publicKeyHex: String, timeStamp: Long = currentSystemTimestamp()): Event {
19 | //val currentUnixTime = currentSystemTimestamp()
20 | val privKey = privateKeyHex.toBytes()
21 | val genPubKey = Secp256k1.pubkeyCreate(privKey).drop(1).take(32).toByteArray()
22 | val genPubKeyHex = genPubKey.toHexString()
23 | if (genPubKeyHex != publicKeyHex)
24 | throw EventValidationError("The pubkeys don't match. Expected :$publicKeyHex \n Generated:$genPubKeyHex ")
25 | val eventID = getEventId(publicKeyHex, timeStamp, eventKind, tags, content)
26 |
27 | val eventIDRaw = eventID.toBytes()
28 | val signature = CryptoUtils.signContent(privateKeyHex.toBytes(), eventIDRaw)
29 | val signatureString = signature.toHexString()
30 | val normalizedTags = tags.map {
31 | Tag(it.identifier,
32 | it.description,
33 | it.content,
34 | it.customContent)
35 | }
36 |
37 | return Event(eventID, publicKeyHex, timeStamp, eventKind, normalizedTags, content, signatureString)
38 | }
39 |
40 | @JvmStatic
41 | fun MetadataEvent(privkey: String,
42 | pubkey: String,
43 | timeStamp: Long = currentSystemTimestamp(),
44 | tags: List = emptyList(),
45 | kind: Int = EventKind.METADATA.kind, profile: String): Event {
46 |
47 | return generateEvent(kind, tags, profile, privkey, pubkey, timeStamp)
48 | }
49 |
50 | @JvmStatic
51 | fun TextEvent(privkey: String, pubkey: String,
52 | tags: List = emptyList(),
53 | timeStamp: Long = currentSystemTimestamp(),
54 | kind: Int = EventKind.TEXT_NOTE.kind, content: String): Event {
55 | return generateEvent(kind, tags, content, privkey, pubkey, timeStamp)
56 | }
57 |
58 | @JvmStatic
59 | fun RelayRecommendationEvent(privkey: String, pubkey: String,
60 | tags: List = emptyList(),
61 | timeStamp: Long = currentSystemTimestamp(),
62 | kind: Int = EventKind.RELAY_RECOMMENDATION.kind,
63 | content: String): Event {
64 | if (!(content.startsWith("wss") || content.startsWith("ws"))) {
65 | throw EventValidationError("Content $content is not a valid relay URL.")
66 | }
67 | return generateEvent(kind, tags, content, privkey, pubkey, timeStamp)
68 | }
69 |
70 | @JvmStatic
71 | fun FollowEvent(privkey: String, pubkey: String,
72 | tags: List,
73 | timeStamp: Long = currentSystemTimestamp(),
74 | kind: Int = EventKind.CONTACT_LIST.kind,
75 | content: String): Event {
76 | return generateEvent(kind, tags, content, privkey, pubkey, timeStamp)
77 | }
78 |
79 | @JvmStatic
80 | fun DirectMessageEvent(privkey: String, pubkey: String,
81 | tags: List = emptyList(),
82 | timeStamp: Long = currentSystemTimestamp(),
83 | kind: Int = EventKind.ENCRYPTED_DM.kind,
84 | content: String): Event {
85 | return generateEvent(kind, tags, content, privkey, pubkey, timeStamp)
86 | }
87 |
88 | @JvmStatic
89 | fun DeletionEvent(privkey: String, pubkey: String,
90 | tags: List = emptyList(),
91 | timeStamp: Long = currentSystemTimestamp(),
92 | kind: Int = EventKind.MARKED_FOR_DELETION.kind,
93 | content: String): Event {
94 | return generateEvent(kind, tags, content, privkey, pubkey, timeStamp)
95 | }
96 |
97 | @JvmStatic
98 | fun AuthEvent(privkey: String, pubkey: String,
99 | relayUrl: String, challengeString: String,
100 | timeStamp: Long = currentSystemTimestamp()
101 | ): Event {
102 | val authEventTags = mutableListOf()
103 | authEventTags.add(Tag("relay", relayUrl))
104 | authEventTags.add(Tag("challenge", challengeString))
105 | return generateEvent(EventKind.AUTH.kind, authEventTags, "", privkey, pubkey, timeStamp)
106 | }
107 |
108 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonTest/kotlin/rhodium/nostr/NostrTests.kt:
--------------------------------------------------------------------------------
1 | package rhodium.nostr
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 | import kotlin.test.assertTrue
6 |
7 | class NostrTests {
8 | @Test
9 | fun `it correctly generates the event id`() {
10 | val eventTestTag = listOf(
11 | Tag("p", "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d")
12 | )
13 | val rawEventTestData = listOf(
14 | "f86c44a2de95d9149b51c6a29afeabba264c18e2fa7c49de93424a0c56947785",
15 | "1640839235", "4", "uRuvYr585B80L6rSJiHocw==?iv=oh6LVqdsYYol3JfFnXTbPA=="
16 | )
17 | val eventId = getEventId(
18 | rawEventTestData[0], rawEventTestData[1].toLong(),
19 | rawEventTestData[2].toInt(), eventTestTag, rawEventTestData[3]
20 | )
21 | println("Test Event Id: $eventId")
22 | assertTrue(eventId == "2be17aa3031bdcb006f0fce80c146dea9c1c0268b0af2398bb673365c6444d45")
23 | }
24 |
25 | @Test
26 | fun `it generates a correct event`() {
27 | val eventTestTag = listOf(
28 | Tag("p", "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d")
29 | )
30 | val rawEventTestData = listOf(
31 | "f86c44a2de95d9149b51c6a29afeabba264c18e2fa7c49de93424a0c56947785",
32 | "1640839235", "4", "uRuvYr585B80L6rSJiHocw==?iv=oh6LVqdsYYol3JfFnXTbPA==",
33 | "a5d9290ef9659083c490b303eb7ee41356d8778ff19f2f91776c8dc4443388a64ffcf336e61af4c25c05ac3ae952d1ced889ed655b67790891222aaa15b99fdd"
34 | )
35 | val eventId = getEventId(
36 | rawEventTestData[0], rawEventTestData[1].toLong(),
37 | rawEventTestData[2].toInt(), eventTestTag, rawEventTestData[3]
38 | )
39 | val testEvent = Event(
40 | eventId, rawEventTestData[0], rawEventTestData[1].toLong(),
41 | rawEventTestData[2].toInt(), eventTestTag, rawEventTestData[3], rawEventTestData[4]
42 | )
43 | val eventJson = testEvent.serialize()
44 | val correctTestEventJson =
45 | """{"id":"2be17aa3031bdcb006f0fce80c146dea9c1c0268b0af2398bb673365c6444d45","pubkey":"f86c44a2de95d9149b51c6a29afeabba264c18e2fa7c49de93424a0c56947785","created_at":1640839235,"kind":4,"tags":[["p","13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d"]],"content":"uRuvYr585B80L6rSJiHocw==?iv=oh6LVqdsYYol3JfFnXTbPA==","sig":"a5d9290ef9659083c490b303eb7ee41356d8778ff19f2f91776c8dc4443388a64ffcf336e61af4c25c05ac3ae952d1ced889ed655b67790891222aaa15b99fdd"}"""
46 | println(eventJson)
47 | println(" ")
48 | println(correctTestEventJson)
49 | assertEquals(correctTestEventJson, eventJson)
50 | }
51 |
52 | @Test
53 | fun `it can verify an event`() {
54 |
55 | val testEvent = Event(
56 | "2be17aa3031bdcb006f0fce80c146dea9c1c0268b0af2398bb673365c6444d45",
57 | "f86c44a2de95d9149b51c6a29afeabba264c18e2fa7c49de93424a0c56947785",
58 | 1640839235, EventKind.ENCRYPTED_DM.kind,
59 | listOf(
60 | Tag("p", "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d")
61 | ),
62 | "uRuvYr585B80L6rSJiHocw==?iv=oh6LVqdsYYol3JfFnXTbPA==",
63 | "a5d9290ef9659083c490b303eb7ee41356d8778ff19f2f91776c8dc4443388a64ffcf336e61af4c25c05ac3ae952d1ced889ed655b67790891222aaa15b99fdd"
64 | )
65 | val generatedEventId = getEventId(
66 | testEvent.pubkey, testEvent.creationDate,
67 | testEvent.eventKind, testEvent.tags, testEvent.content
68 | )
69 | assertEquals(testEvent.id, generatedEventId)
70 | }
71 |
72 | @Test
73 | fun `it can generate a subscription event`() {
74 |
75 | }
76 |
77 | @Test
78 | fun `it can correctly parse a received event`() {
79 | val testEventJson =
80 | """{"id":"2be17aa3031bdcb006f0fce80c146dea9c1c0268b0af2398bb673365c6444d45","pubkey":"f86c44a2de95d9149b51c6a29afeabba264c18e2fa7c49de93424a0c56947785","created_at":1640839235,"kind":4,"tags":[["p","13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d"]],"content":"uRuvYr585B80L6rSJiHocw==?iv=oh6LVqdsYYol3JfFnXTbPA==","sig":"a5d9290ef9659083c490b303eb7ee41356d8778ff19f2f91776c8dc4443388a64ffcf336e61af4c25c05ac3ae952d1ced889ed655b67790891222aaa15b99fdd"}"""
81 | val correctlyParsedEvent = Event(
82 | "2be17aa3031bdcb006f0fce80c146dea9c1c0268b0af2398bb673365c6444d45",
83 | "f86c44a2de95d9149b51c6a29afeabba264c18e2fa7c49de93424a0c56947785",
84 | 1640839235, EventKind.ENCRYPTED_DM.kind,
85 | listOf(
86 | Tag("p", "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d")
87 | ),
88 | "uRuvYr585B80L6rSJiHocw==?iv=oh6LVqdsYYol3JfFnXTbPA==",
89 | "a5d9290ef9659083c490b303eb7ee41356d8778ff19f2f91776c8dc4443388a64ffcf336e61af4c25c05ac3ae952d1ced889ed655b67790891222aaa15b99fdd"
90 | )
91 | val event = deserializedEvent(testEventJson)
92 | println("Des. Event :")
93 | println(event)
94 | println("Correct Event:")
95 | println(correctlyParsedEvent)
96 | assertEquals(correctlyParsedEvent.toString(), event.toString())
97 | }
98 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/nostr/Event.kt:
--------------------------------------------------------------------------------
1 |
2 | package rhodium.nostr
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 | import kotlinx.serialization.builtins.ListSerializer
7 | import kotlinx.serialization.json.add
8 | import kotlinx.serialization.json.buildJsonArray
9 | import rhodium.crypto.CryptoUtils
10 | import rhodium.crypto.toHexString
11 |
12 | /**
13 | * The Event class representing the Nostr Event.
14 | * The Event is of the form:
15 | * Event(event_id, pubkey, creation_date, kind, tags, content, signature)
16 | * @param id The event id, as a string
17 | * @param pubkey The public key of the event's author, as a 32-byte string
18 | * @param creationDate The Unix timestamp of the event, as a number
19 | * @param eventKind The event kind, as a number
20 | * @param tags The list of tags associated with the event
21 | * @param content The event's content, as a string
22 | * @param eventSignature The event's signature, as a 64-byte string
23 | */
24 | @Serializable
25 | open class Event(
26 | val id: String,
27 | val pubkey: String,
28 | @SerialName("created_at") val creationDate: Long,
29 | @SerialName("kind") val eventKind: Int,
30 | val tags: List,
31 | val content: String,
32 | @SerialName("sig") val eventSignature: String
33 | ){
34 | override fun toString(): String =
35 | "Event(id=$id, pubkey=$pubkey, creationDate=$creationDate, eventKind=$eventKind, tags=$tags, content=$content, eventSignature=$eventSignature)"
36 | }
37 |
38 |
39 |
40 | internal fun getEventId(
41 | pubkey: String, timeStamp: Long, eventKind: Int,
42 | tags: List, content: String
43 | ): String {
44 | val jsonToHash = rawEventJson0(pubkey, timeStamp, eventKind, tags, content)
45 |
46 | val jsonHash = CryptoUtils.contentHash(jsonToHash)
47 | return jsonHash.toHexString()
48 | }
49 |
50 | internal fun rawEventJson0(
51 | pubkey: String, timeStamp: Long, eventKind: Int,
52 | tags: List, content: String
53 | ): String {
54 | val serializedRawEvent = buildJsonArray {
55 | add(0)
56 | add(pubkey)
57 | add(timeStamp)
58 | add(eventKind)
59 | val tagListElement = eventMapper.encodeToJsonElement(ListSerializer(Tag.TagSerializer()), tags)
60 | add(tagListElement)
61 | add(content)
62 | }
63 | return serializedRawEvent.toString()
64 | }
65 |
66 |
67 | /**
68 | * This function is kept here for legacy purposes
69 | */
70 | //internal fun rawEventJson(pubkey: String, timeStamp: Long, eventKind: Int,
71 | // tags: List>, content: String): String {
72 | //
73 | // val listOfTagsJson = if (tags.isEmpty()) "[]"
74 | // else buildString {
75 | // append("[")
76 | // tags.forEach { tagArray ->
77 | // if(tagArray.size < 2) throw Error("Invalid tag structure")
78 | // if (tagArray.size ==3) {
79 | // val tag = Tag(tagArray[0], tagArray[1], tagArray[2])
80 | // append(
81 | // "[\"${tag.identifier}\",\"${tag.description}\"${
82 | // if (tag.recommendedRelayUrl.isNullOrEmpty()) ""
83 | // else ",\"${tag.recommendedRelayUrl}\""
84 | // }]"
85 | // )
86 | // append(",")
87 | // }
88 | //
89 | // }
90 | // deleteAt(lastIndexOf(","))
91 | // append("]")
92 | // }
93 | //
94 | // val jsonEventRaw = buildString {
95 | // append("[")
96 | // append("0,")
97 | // append("\"$pubkey\",")
98 | // append("$timeStamp,")
99 | // append("$eventKind,")
100 | // append("$listOfTagsJson,")
101 | // append("\"$content\"")
102 | // append("]")
103 | // }
104 | // return jsonEventRaw
105 | //}
106 |
107 |
108 | /**
109 | * This object represents the various event kinds
110 | * currently used on Nostr. Not all are supported, though.
111 | */
112 | enum class EventKind(val kind: Int) {
113 | /**
114 | * Represents profile creation and modification on Nostr.
115 | */
116 | METADATA(0),
117 |
118 | /**
119 | * Represents published notes or posts or 'tweets'.
120 | */
121 | TEXT_NOTE(1),
122 |
123 | /**
124 | * Represents relay recommendations, for sharing relays. For helping with censorship-resistance.
125 | */
126 | RELAY_RECOMMENDATION(2),
127 |
128 | /**
129 | * Represents contact lists, for sharing profiles, and (probably) building
130 | * friend lists.
131 | */
132 | CONTACT_LIST(3),
133 |
134 | /**
135 | * Represents encrypted messages.
136 | */
137 | ENCRYPTED_DM(4),
138 |
139 | /**
140 | * Represents posts marked for deletion.
141 | */
142 | MARKED_FOR_DELETION(5),
143 |
144 | /**
145 | * Represents a profile's relay list.
146 | */
147 | RELAY_LIST(10002),
148 |
149 | /**
150 | * This is used for auth events constructed by the client.
151 | */
152 | AUTH(22242),
153 |
154 | /**
155 | * This is used for comments on any posts other than Kind 1 posts(i.e, blogposts, shared file events,etc).
156 | */
157 | COMMENT(1111);
158 |
159 | }
160 |
--------------------------------------------------------------------------------
/rhodium-core/src/commonTest/kotlin/rhodium/nostr/client/ClientMessageTests.kt:
--------------------------------------------------------------------------------
1 | package rhodium.nostr.client
2 |
3 | import kotlinx.serialization.encodeToString
4 | import kotlinx.serialization.json.Json
5 | import rhodium.nostr.EventKind
6 | import rhodium.nostr.NostrFilter
7 | import kotlin.jvm.JvmStatic
8 | import kotlin.test.Test
9 | import kotlin.test.assertEquals
10 |
11 | data class TV(val nostrFilters: List, val nostrFilterJson: String)
12 |
13 | class ClientMessageTests {
14 | private val testEventMapper = Json
15 | private fun stringifyMessage(message: ClientMessage) = testEventMapper.encodeToString(message)
16 |
17 | @Test
18 | fun `close request decodes properly`(){
19 | val closeRequest = CloseRequest(subscriptionId = "mySub")
20 | val closeRequestJson = """["CLOSE", "mySub"]"""
21 | val decodedCloseRequest = testEventMapper.decodeFromString(closeRequestJson)
22 | assertEquals(closeRequest, decodedCloseRequest)
23 | }
24 |
25 | @Test
26 | fun `single filter request message encodes properly`(){
27 | val requestMessage = RequestMessage(subscriptionId = "mySub", filters = testVectors().first().nostrFilters)
28 | val resultingJson = stringifyMessage(requestMessage)
29 | assertEquals(testVectors().first().nostrFilterJson, resultingJson)
30 | }
31 |
32 | @Test
33 | fun `single filter request message decodes properly`(){
34 | val filterRequestJson = testVectors().first().nostrFilterJson
35 | val resultingFilter = testEventMapper.decodeFromString(filterRequestJson)
36 | val correctRequest = RequestMessage(subscriptionId = "mySub", filters = testVectors().first().nostrFilters)
37 | assertEquals(correctRequest.toString(), resultingFilter.toString())
38 | }
39 |
40 | @Test
41 | fun `multiple filter request message encodes properly`(){
42 | val requestMessage =
43 | RequestMessage(subscriptionId = "mySub", filters = testVectors().last().nostrFilters)
44 | val requestJson = stringifyMessage(requestMessage)
45 | assertEquals(testVectors().last().nostrFilterJson, requestJson)
46 | }
47 |
48 | @Test
49 | fun `single filter auth request encodes properly`(){
50 | val countRequest = CountRequest(subscriptionId = "mySub", countFilters = countTestVectors().first().nostrFilters)
51 | val correspondingJson = stringifyMessage(countRequest)
52 | assertEquals(countTestVectors().first().nostrFilterJson, correspondingJson)
53 | }
54 |
55 | @Test
56 | fun `multiple filter auth request encodes properly`(){
57 | val countRequest = CountRequest(subscriptionId = "mySub", countFilters = countTestVectors().last().nostrFilters)
58 | val correspondingJson = stringifyMessage(countRequest)
59 | assertEquals(countTestVectors().last().nostrFilterJson, correspondingJson)
60 | }
61 |
62 | companion object {
63 | private val filterOne = NostrFilter.newFilter()
64 | .idList("event_id_1", "event_id_2", "event_id_3")
65 | .authors("author_pubkey_1", "author_pubkey_2")
66 | .kinds(EventKind.TEXT_NOTE.kind)
67 | .eventTagList("ref_event_id_1", "ref_event_id_2")
68 | .pubkeyTagList("ref_pubkey_1")
69 | .since(1653822739L - 24 * 60 * 60)
70 | .until(1653822739L)
71 | .limit(25)
72 | .build()
73 |
74 | private val filterTwo = NostrFilter.newFilter()
75 | .idList("event_id_4", "event_id_5")
76 | .authors("author_pubkey_3", "author_pubkey_4")
77 | .kinds(EventKind.METADATA.kind, EventKind.RELAY_RECOMMENDATION.kind)
78 | .eventTagList("ref_event_id_3", "ref_event_id_4")
79 | .pubkeyTagList("ref_pubkey_2", "ref_pubkey_3", "ref_pubkey_4")
80 | .since(1653822739L - 24 * 60 * 60)
81 | .until(1653822739L)
82 | .limit(10)
83 | .build()
84 |
85 | @JvmStatic
86 | fun countTestVectors() = listOf(
87 | TV(listOf(filterOne), """["COUNT","mySub",{"ids":["event_id_1","event_id_2","event_id_3"],"authors":["author_pubkey_1","author_pubkey_2"],"kinds":[1],"#e":["ref_event_id_1","ref_event_id_2"],"#p":["ref_pubkey_1"],"since":1653736339,"until":1653822739,"limit":25}]"""),
88 | TV(listOf(filterOne, filterTwo), """["COUNT","mySub",{"ids":["event_id_1","event_id_2","event_id_3"],"authors":["author_pubkey_1","author_pubkey_2"],"kinds":[1],"#e":["ref_event_id_1","ref_event_id_2"],"#p":["ref_pubkey_1"],"since":1653736339,"until":1653822739,"limit":25},{"ids":["event_id_4","event_id_5"],"authors":["author_pubkey_3","author_pubkey_4"],"kinds":[0,2],"#e":["ref_event_id_3","ref_event_id_4"],"#p":["ref_pubkey_2","ref_pubkey_3","ref_pubkey_4"],"since":1653736339,"until":1653822739,"limit":10}]""")
89 | )
90 |
91 | @JvmStatic
92 | fun testVectors() = listOf(
93 | TV(listOf(filterOne), """["REQ","mySub",{"ids":["event_id_1","event_id_2","event_id_3"],"authors":["author_pubkey_1","author_pubkey_2"],"kinds":[1],"#e":["ref_event_id_1","ref_event_id_2"],"#p":["ref_pubkey_1"],"since":1653736339,"until":1653822739,"limit":25}]"""),
94 | TV(listOf(filterTwo), """["REQ","mySub",{"ids":["event_id_4","event_id_5"],"authors":["author_pubkey_3","author_pubkey_4"],"kinds":[0,2],"#e":["ref_event_id_3","ref_event_id_4"],"#p":["ref_pubkey_2","ref_pubkey_3","ref_pubkey_4"],"since":1653736339,"until":1653822739,"limit":10}]"""),
95 | TV(listOf(filterOne, filterTwo), """["REQ","mySub",{"ids":["event_id_1","event_id_2","event_id_3"],"authors":["author_pubkey_1","author_pubkey_2"],"kinds":[1],"#e":["ref_event_id_1","ref_event_id_2"],"#p":["ref_pubkey_1"],"since":1653736339,"until":1653822739,"limit":25},{"ids":["event_id_4","event_id_5"],"authors":["author_pubkey_3","author_pubkey_4"],"kinds":[0,2],"#e":["ref_event_id_3","ref_event_id_4"],"#p":["ref_pubkey_2","ref_pubkey_3","ref_pubkey_4"],"since":1653736339,"until":1653822739,"limit":10}]""")
96 | )
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/rhodium-core/src/commonTest/kotlin/rhodium/nostr/NostrFilterTest.kt:
--------------------------------------------------------------------------------
1 | package rhodium.nostr
2 |
3 | import kotlinx.serialization.encodeToString
4 | import kotlinx.serialization.json.Json
5 | import rhodium.currentSystemTimestamp
6 | import kotlin.test.Test
7 | import kotlin.test.assertEquals
8 |
9 | class NostrFilterTest {
10 | private val nostrFilterEventMapper = Json
11 |
12 | // For the first filter
13 | private val eventIdList = listOf("event_id_1", "event_id_2", "event_id_3")
14 | private val authorList = listOf("author_pubkey_1", "author_pubkey_2")
15 | private val listOfKinds = listOf(EventKind.TEXT_NOTE.kind)
16 | private val referencedEventIds = listOf("ref_event_id_1", "ref_event_id_2")
17 | private val referencedProfiles = listOf("ref_pubkey_1")
18 | private val upperTimeLimit = currentSystemTimestamp()
19 | private val lowerTimeLimit = upperTimeLimit - 24 * 60 * 60
20 | private val maxEventLimit = 25
21 |
22 |
23 | // For the second filter
24 | // private val secondEventIdList = listOf("event_id_4", "event_id_5")
25 | // private val secondAuthorList = listOf("author_pubkey_3", "author_pubkey_4")
26 | // private val secondKindList = listOf(EventKind.METADATA, EventKind.RELAY_RECOMMENDATION)
27 | // private val referencedEventIdList = listOf("ref_event_id_3", "ref_event_id_4")
28 | // private val referencedProfileList = listOf("ref_pubkey_2", "ref_pubkey_3", "ref_pubkey_4")
29 | // private val secondMaxEventLimit = 10
30 | // val filter_2 = NostrFilter(secondEventIdList, secondAuthorList, secondKindList, referencedEventIdList,
31 | // referencedProfileList, lowerTimeLimit, upperTimeLimit, secondMaxEventLimit)
32 |
33 | @Test
34 | fun `it serializes the nostr filter correctly`() {
35 | val currentTimestamp = 1653822739L
36 | val previousTimestamp = currentTimestamp - 24 * 60 * 60
37 | val filter = NostrFilter.newFilter()
38 | .idList(*eventIdList.toTypedArray())
39 | .authors(*authorList.toTypedArray())
40 | .kinds(*listOfKinds.toIntArray())
41 | .eventTagList(*referencedEventIds.toTypedArray())
42 | .pubkeyTagList(*referencedProfiles.toTypedArray())
43 | .since(previousTimestamp)
44 | .until(currentTimestamp)
45 | .limit(maxEventLimit)
46 | .build()
47 |
48 | val correctFilterJson =
49 | """{"ids":["event_id_1","event_id_2","event_id_3"],"authors":["author_pubkey_1","author_pubkey_2"],"kinds":[1],"#e":["ref_event_id_1","ref_event_id_2"],"#p":["ref_pubkey_1"],"since":1653736339,"until":1653822739,"limit":25}"""
50 |
51 | val filterJson = nostrFilterEventMapper.encodeToString(filter)
52 | println("Correct filterJson: \n $correctFilterJson")
53 | println("Generated filterJson: \n $filterJson")
54 | assertEquals(filterJson, correctFilterJson)
55 |
56 | }
57 |
58 | @Test
59 | fun `the timestamp for the filter is correctly generated`() {
60 | val filter = NostrFilter.newFilter()
61 | .idList(*eventIdList.toTypedArray())
62 | .authors(*authorList.toTypedArray())
63 | .kinds(*listOfKinds.toIntArray())
64 | .eventTagList(*referencedEventIds.toTypedArray())
65 | .pubkeyTagList(*referencedProfiles.toTypedArray())
66 | .since(lowerTimeLimit)
67 | .until(upperTimeLimit)
68 | .limit(maxEventLimit)
69 | .build()
70 |
71 | val cloneFilter = NostrFilter.newFilter()
72 | .idList(*eventIdList.toTypedArray())
73 | .authors(*authorList.toTypedArray())
74 | .kinds(*listOfKinds.toIntArray())
75 | .eventTagList(*referencedEventIds.toTypedArray())
76 | .pubkeyTagList(*referencedProfiles.toTypedArray())
77 | .since(lowerTimeLimit)
78 | .until(upperTimeLimit)
79 | .limit(maxEventLimit)
80 | .build()
81 |
82 | val filterJson = nostrFilterEventMapper.encodeToString(filter)
83 | val cloneFilterJson = nostrFilterEventMapper.encodeToString(cloneFilter)
84 |
85 | println(filter)
86 | println(cloneFilter)
87 | println("FilterJson: $filterJson")
88 | println("Clone filterJson: $cloneFilterJson")
89 | assertEquals(filterJson, cloneFilterJson)
90 | }
91 |
92 | @Test
93 | fun `another test for correct serialization`(){
94 | val currentTimestamp = 1653822739L
95 | val previousTimestamp = currentTimestamp - 24 * 60 * 60
96 | val textEventFilter = NostrFilter.newFilter()
97 | .idList()
98 | .authors()
99 | .kinds(EventKind.TEXT_NOTE.kind)
100 | .eventTagList()
101 | .pubkeyTagList()
102 | .since(previousTimestamp)
103 | .until(currentTimestamp)
104 | .limit(30)
105 | .build()
106 | val filterJson = nostrFilterEventMapper.encodeToString(textEventFilter)
107 | val correctRequestJson = """{"kinds":[1],"since":1653736339,"until":1653822739,"limit":30}"""
108 | println(filterJson)
109 | assertEquals(correctRequestJson, filterJson)
110 | }
111 |
112 | @Test
113 | fun `filter with search parameter serializes correctly`(){
114 | val currentTimestamp = 1653822739L
115 | val previousTimestamp = currentTimestamp - 24 * 60 * 60
116 | val searchFilter = NostrFilter.newFilter()
117 | .kinds(EventKind.TEXT_NOTE.kind)
118 | .search("bitcoin")
119 | .since(previousTimestamp)
120 | .until(currentTimestamp)
121 | .limit(10)
122 | .build()
123 | val filterJson = nostrFilterEventMapper.encodeToString(searchFilter)
124 | val correctSearchJson = """{"kinds":[1],"since":1653736339,"until":1653822739,"search":"bitcoin","limit":10}"""
125 | println("Search filter JSON: $filterJson")
126 | assertEquals(correctSearchJson, filterJson)
127 | }
128 |
129 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MSYS* | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | ### Android ###
3 | # Built application files
4 | *.apk
5 | *.aar
6 | *.ap_
7 | *.aab
8 |
9 | # Files for the ART/Dalvik VM
10 | *.dex
11 |
12 | # Java class files
13 | *.class
14 |
15 | # Generated files
16 | bin/
17 | gen/
18 | out/
19 | # Uncomment the following line in case you need and you don't have the release build type files in your app
20 | # release/
21 |
22 | # Gradle files
23 | .gradle/
24 | build/
25 |
26 | # Local configuration file (sdk path, etc)
27 | local.properties
28 |
29 | # Proguard folder generated by Eclipse
30 | proguard/
31 |
32 | # Log Files
33 | *.log
34 |
35 | # Android Studio Navigation editor temp files
36 | .navigation/
37 |
38 | # Android Studio captures folder
39 | captures/
40 |
41 | # IntelliJ
42 | *.iml
43 | .idea/
44 |
45 | # Keystore files
46 | # Uncomment the following lines if you do not want to check your keystore files in.
47 | #*.jks
48 | #*.keystore
49 |
50 | # External native build folder generated in Android Studio 2.2 and later
51 | .externalNativeBuild
52 | .cxx/
53 |
54 | # Google Services (e.g. APIs or Firebase)
55 | # google-services.json
56 |
57 | # Freeline
58 | freeline.py
59 | freeline/
60 | freeline_project_description.json
61 |
62 | # fastlane
63 | fastlane/report.xml
64 | fastlane/Preview.html
65 | fastlane/screenshots
66 | fastlane/test_output
67 | fastlane/readme.md
68 |
69 | # Version control
70 | vcs.xml
71 |
72 | # lint
73 | lint/intermediates/
74 | lint/generated/
75 | lint/outputs/
76 | lint/tmp/
77 | # lint/reports/
78 |
79 | ### Android Patch ###
80 | gen-external-apklibs
81 | output.json
82 |
83 | # Replacement of .externalNativeBuild directories introduced
84 | # with Android Studio 3.5.
85 |
86 | ### Kotlin ###
87 | .kotlin/
88 | # Compiled class file
89 |
90 | # Log file
91 |
92 | # BlueJ files
93 | *.ctxt
94 |
95 | # Mobile Tools for Java (J2ME)
96 | .mtj.tmp/
97 |
98 | # Package Files #
99 | *.jar
100 | *.war
101 | *.nar
102 | *.ear
103 | *.tar.gz
104 | *.rar
105 |
106 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
107 | hs_err_pid*
108 |
109 | ### Swift ###
110 | # Xcode
111 | #
112 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
113 |
114 | ## User settings
115 | xcuserdata/
116 |
117 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
118 | *.xcscmblueprint
119 | *.xccheckout
120 |
121 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
122 | DerivedData/
123 | *.moved-aside
124 | *.pbxuser
125 | !default.pbxuser
126 | *.mode1v3
127 | !default.mode1v3
128 | *.mode2v3
129 | !default.mode2v3
130 | *.perspectivev3
131 | !default.perspectivev3
132 |
133 | ## Obj-C/Swift specific
134 | *.hmap
135 |
136 | ## App packaging
137 | *.ipa
138 | *.dSYM.zip
139 | *.dSYM
140 |
141 | ## Playgrounds
142 | timeline.xctimeline
143 | playground.xcworkspace
144 |
145 | # Swift Package Manager
146 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
147 | # Packages/
148 | # Package.pins
149 | # Package.resolved
150 | # *.xcodeproj
151 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
152 | # hence it is not needed unless you have added a package configuration file to your project
153 | # .swiftpm
154 |
155 | .build/
156 |
157 | # CocoaPods
158 | # We recommend against adding the Pods directory to your .gitignore. However
159 | # you should judge for yourself, the pros and cons are mentioned at:
160 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
161 | # Pods/
162 | # Add this line if you want to avoid checking in source code from the Xcode workspace
163 | # *.xcworkspace
164 |
165 | # Carthage
166 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
167 | # Carthage/Checkouts
168 |
169 | Carthage/Build/
170 |
171 | # Accio dependency management
172 | Dependencies/
173 | .accio/
174 |
175 | # fastlane
176 | # It is recommended to not store the screenshots in the git repo.
177 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
178 | # For more information about the recommended setup visit:
179 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
180 |
181 | fastlane/screenshots/**/*.png
182 |
183 | # Code Injection
184 | # After new code Injection tools there's a generated folder /iOSInjectionProject
185 | # https://github.com/johnno1962/injectionforxcode
186 |
187 | iOSInjectionProject/
188 |
189 | ### Xcode ###
190 | # Xcode
191 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
192 |
193 |
194 |
195 |
196 | ## Gcc Patch
197 | /*.gcno
198 |
199 | ### Xcode Patch ###
200 | *.xcodeproj/*
201 | !*.xcodeproj/project.pbxproj
202 | !*.xcodeproj/xcshareddata/
203 | !*.xcworkspace/contents.xcworkspacedata
204 | **/xcshareddata/WorkspaceSettings.xcsettings
205 |
206 | ### AndroidStudio ###
207 | # Covers files to be ignored for android development using Android Studio.
208 |
209 | # Built application files
210 |
211 | # Files for the ART/Dalvik VM
212 |
213 | # Java class files
214 |
215 | # Generated files
216 |
217 | # Gradle files
218 | .gradle
219 |
220 | # Signing files
221 | .signing/
222 |
223 | # Local configuration file (sdk path, etc)
224 |
225 | # Proguard folder generated by Eclipse
226 |
227 | # Log Files
228 |
229 | # Android Studio
230 | /*/build/
231 | /*/local.properties
232 | /*/out
233 | /*/*/build
234 | /*/*/production
235 | *.ipr
236 | *~
237 | *.swp
238 |
239 | # Keystore files
240 | *.jks
241 | *.keystore
242 |
243 | # Google Services (e.g. APIs or Firebase)
244 | # google-services.json
245 |
246 | # Android Patch
247 |
248 | # External native build folder generated in Android Studio 2.2 and later
249 |
250 | # NDK
251 | obj/
252 |
253 | # IntelliJ IDEA
254 | *.iws
255 | /out/
256 |
257 | # OS-specific files
258 | .DS_Store
259 | .DS_Store?
260 | ._*
261 | .Spotlight-V100
262 | .Trashes
263 | ehthumbs.db
264 | Thumbs.db
265 |
266 | # Legacy Eclipse project files
267 | .classpath
268 | .project
269 | .cproject
270 | .settings/
271 |
272 | # Mobile Tools for Java (J2ME)
273 |
274 | # Package Files #
275 |
276 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
277 |
278 | ## Plugin-specific files:
279 |
280 | # mpeltonen/sbt-idea plugin
281 | .idea_modules/
282 |
283 | # JIRA plugin
284 | atlassian-ide-plugin.xml
285 |
286 | # Crashlytics plugin (for Android Studio and IntelliJ)
287 | com_crashlytics_export_strings.xml
288 | crashlytics.properties
289 | crashlytics-build.properties
290 | fabric.properties
291 |
292 | ### AndroidStudio Patch ###
293 |
294 | !/gradle/wrapper/gradle-wrapper.jar
295 |
296 | # Swiftlint files
297 | .swiftlint/
298 | !swiftlint-wrapper.jar
299 |
300 | # End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,xcode,swift,kotlin
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/nostr/client/ClientMessage.kt:
--------------------------------------------------------------------------------
1 | package rhodium.nostr.client
2 |
3 | import com.benasher44.uuid.bytes
4 | import com.benasher44.uuid.uuid4
5 | import kotlinx.serialization.ExperimentalSerializationApi
6 | import kotlinx.serialization.KSerializer
7 | import kotlinx.serialization.Serializable
8 | import kotlinx.serialization.SerializationException
9 | import kotlinx.serialization.builtins.ListSerializer
10 | import kotlinx.serialization.builtins.serializer
11 | import kotlinx.serialization.descriptors.SerialDescriptor
12 | import kotlinx.serialization.encoding.Decoder
13 | import kotlinx.serialization.encoding.Encoder
14 | import kotlinx.serialization.json.*
15 | import rhodium.crypto.toHexString
16 | import rhodium.nostr.Event
17 | import rhodium.nostr.NostrFilter
18 |
19 | @Serializable(with = ClientMessage.MessageSerializer::class)
20 | sealed class ClientMessage(open val messageType: String){
21 | companion object MessageSerializer : KSerializer {
22 | private val listSerializer = ListSerializer(elementSerializer = String.serializer())
23 |
24 | override val descriptor: SerialDescriptor
25 | get() = listSerializer.descriptor
26 |
27 | override fun deserialize(decoder: Decoder): ClientMessage {
28 | val jsonDecoder = (decoder as JsonDecoder)
29 | val message = jsonDecoder.decodeJsonElement().jsonArray
30 | val marker = message[0].jsonPrimitive.content
31 | println("Message type : $marker")
32 | val clientMessage = when(marker) {
33 |
34 | "REQ" -> {
35 | val requestMessage = decodeToRequestMessage(jsonDecoder, message)
36 | requestMessage
37 | }
38 | "EVENT" -> {
39 | val clientEventMessage = decodeToEventMessage(jsonDecoder, message)
40 | clientEventMessage
41 | }
42 | "CLOSE" -> {
43 | val closeRequest = decodeToCloseRequest(jsonDecoder, message)
44 | closeRequest
45 | }
46 | else -> {
47 | throw SerializationException("Unrecognized message type: $marker")
48 | }
49 | }
50 | return clientMessage
51 | }
52 |
53 | @OptIn(ExperimentalSerializationApi::class)
54 | override fun serialize(encoder: Encoder, value: ClientMessage) {
55 | val jsonElementEncoder = (encoder as JsonEncoder).json
56 | val encodedMessage = buildJsonArray {
57 | val type = jsonElementEncoder.encodeToJsonElement(value.messageType)
58 | add(type)
59 | when(value){
60 | is ClientEventMessage -> {
61 | val eventElement = jsonElementEncoder.encodeToJsonElement(Event.serializer(), value.event)
62 | add(eventElement)
63 | }
64 | is CloseRequest -> {
65 | val idElement = jsonElementEncoder.encodeToJsonElement(value.subscriptionId)
66 | add(idElement)
67 | }
68 | is RequestMessage -> {
69 | val idElement = jsonElementEncoder.parseToJsonElement(value.subscriptionId)
70 | val filtersElement = value.filters?.map { filter ->
71 | jsonElementEncoder.encodeToJsonElement(NostrFilter.serializer(), filter)
72 | }
73 | add(idElement)
74 | if (filtersElement != null) {
75 | addAll(filtersElement)
76 | } else {
77 | error("NostrFilter error: could not build filter.")
78 | }
79 | }
80 | }
81 | }
82 | encoder.encodeJsonElement(encodedMessage)
83 | }
84 | }
85 | }
86 |
87 | @Serializable(with = ClientMessage.MessageSerializer::class)
88 | data class ClientAuthMessage(
89 | override val messageType: String = "AUTH",
90 | val authEvent: Event
91 | ): ClientEventMessage(messageType, authEvent)
92 |
93 | @Serializable(with = ClientMessage.MessageSerializer::class)
94 | open class ClientEventMessage(
95 | override val messageType: String = "EVENT",
96 | val event: Event
97 | ) : ClientMessage(messageType) {
98 | override fun toString() = """
99 | Message type -> $messageType
100 | Event -> $event
101 | """.trimIndent()
102 | }
103 |
104 | @Serializable(with = ClientMessage.MessageSerializer::class)
105 | open class RequestMessage(
106 | override val messageType: String = "REQ",
107 | open val subscriptionId: String = uuid4().bytes.decodeToString().substring(0, 5),
108 | val filters: List?
109 | ) : ClientMessage(messageType) {
110 | override fun toString() = """
111 | Message type -> $messageType
112 | SubscriptionId -> $subscriptionId
113 | Filters -> $filters
114 | """.trimIndent()
115 |
116 | companion object {
117 | fun singleFilterRequest(
118 | subscriptionId: String = uuid4().bytes.toHexString().substring(0, 5),
119 | filter: NostrFilter
120 | ): RequestMessage {
121 | return RequestMessage(messageType = "REQ", subscriptionId, listOf(filter))
122 | }
123 | }
124 | }
125 |
126 | @Serializable(with = ClientMessage.MessageSerializer::class)
127 | data class CountRequest(
128 | override val messageType: String = "COUNT",
129 | override val subscriptionId: String,
130 | val countFilters: List?
131 | ): RequestMessage(messageType, subscriptionId, filters = countFilters)
132 |
133 | @Serializable(with = ClientMessage.MessageSerializer::class)
134 | data class CloseRequest(
135 | override val messageType: String = "CLOSE",
136 | val subscriptionId: String
137 | ) : ClientMessage(messageType)
138 |
139 | private fun decodeToRequestMessage(jsonDecoder: JsonDecoder, jsonArray: JsonArray): RequestMessage {
140 | var requestMarker: String? = null
141 | var subscriptionId: String? = null
142 | val filterList = mutableListOf()
143 |
144 | jsonArray.forEachIndexed { index, element ->
145 | when(index){
146 | 0 -> {
147 | val requestMarkerElement = element.jsonPrimitive
148 | if (requestMarkerElement.isString){
149 | requestMarker = requestMarkerElement.content
150 | } else {
151 | error("Cannot decode this request filter. Marker content: ${requestMarkerElement.content}")
152 | }
153 | }
154 | 1 -> {
155 | val idStringElement = element.jsonPrimitive
156 | subscriptionId = idStringElement.content
157 | }
158 | else -> {
159 | val filterElement = jsonDecoder.json.decodeFromJsonElement(element)
160 | filterList.add(filterElement)
161 | }
162 | }
163 | }
164 | return RequestMessage(requestMarker.toString(), subscriptionId.toString(), filterList)
165 | }
166 |
167 | private fun decodeToEventMessage(jsonDecoder: JsonDecoder, jsonArray: JsonArray): ClientEventMessage {
168 | val eventMarker = jsonArray[0].jsonPrimitive.content
169 | val event = jsonDecoder.json.decodeFromJsonElement(Event.serializer(), jsonArray[1])
170 | return ClientEventMessage(eventMarker, event)
171 | }
172 |
173 | private fun decodeToCloseRequest(jsonDecoder: JsonDecoder, jsonArray: JsonArray): CloseRequest {
174 | val closeMarker = jsonArray[0].jsonPrimitive.content
175 | val subscriptionId = jsonDecoder.json.decodeFromJsonElement(String.serializer(), jsonArray[1])
176 | return CloseRequest(closeMarker, subscriptionId)
177 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/Nip19Parser.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 KotlinGeekDev
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | *
22 | */
23 |
24 | package rhodium.crypto
25 |
26 | /**
27 | * Copyright (c) 2024 Vitor Pamplona
28 | *
29 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
30 | * this software and associated documentation files (the "Software"), to deal in
31 | * the Software without restriction, including without limitation the rights to use,
32 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
33 | * Software, and to permit persons to whom the Software is furnished to do so,
34 | * subject to the following conditions:
35 | *
36 | * The above copyright notice and this permission notice shall be included in all
37 | * copies or substantial portions of the Software.
38 | *
39 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
41 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
42 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
43 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
44 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45 | */
46 |
47 | import fr.acinq.secp256k1.Hex
48 | import kotlinx.coroutines.CancellationException
49 | import rhodium.crypto.tlv.entity.*
50 | import rhodium.logging.serviceLogger
51 |
52 | object Nip19Parser {
53 | private val nip19PlusNip46regex: Regex =
54 | Regex(
55 | "(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1|nembed1|ncryptsec1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)([\\S]*)",
56 | RegexOption.IGNORE_CASE,
57 | )
58 |
59 | val nip19regex: Regex =
60 | Regex(
61 | "(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1|nembed1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)([\\S]*)",
62 | RegexOption.IGNORE_CASE,
63 | )
64 |
65 | data class ParseReturn(
66 | val entity: Entity,
67 | val nip19raw: String,
68 | val additionalChars: String? = null,
69 | )
70 |
71 | fun tryParseAndClean(uri: String?): String? {
72 | if (uri == null) return null
73 |
74 | try {
75 | val matcher = nip19PlusNip46regex.find(uri)
76 | if (matcher == null) {
77 | return null
78 | }
79 |
80 | val type = matcher.groups[2]?.value // npub1
81 | val key = matcher.groups[3]?.value // bech32
82 |
83 | return type + key
84 | } catch (e: Throwable) {
85 | serviceLogger.e("NIP19 Parser: Issue trying to Decode NIP19 $uri: ${e.message}", e)
86 | }
87 |
88 | return null
89 | }
90 |
91 | fun parse(uri: String?): ParseReturn? {
92 | if (uri == null) return null
93 |
94 | try {
95 | val matcher = nip19regex.find(uri)
96 | if (matcher == null) {
97 | return null
98 | }
99 |
100 | val type = matcher.groups[2]?.value // npub1
101 | val key = matcher.groups[3]?.value // bech32
102 | val additionalChars = matcher.groups[4]?.value // additional chars
103 |
104 | if (type == null) return null
105 |
106 | return parseComponents(type, key, additionalChars?.ifEmpty { null })
107 | } catch (e: Throwable) {
108 | serviceLogger.e("NIP19 Parser: Issue trying to Decode NIP19 $uri: ${e.message}", e)
109 | }
110 |
111 | return null
112 | }
113 |
114 | fun parseComponents(
115 | type: String,
116 | key: String?,
117 | additionalChars: String?,
118 | ): ParseReturn? =
119 | try {
120 | val nip19 = (type + key)
121 | val bytes = nip19.bechToBytes()
122 |
123 | when (type.lowercase()) {
124 | "nsec1" -> NSec.parse(bytes)
125 | "npub1" -> NPub.parse(bytes)
126 | "note1" -> Note.parse(bytes)
127 | "nprofile1" -> NProfile.parse(bytes)
128 | "nevent1" -> NEvent.parse(bytes)
129 | "nrelay1" -> NRelay.parse(bytes)
130 | "naddr1" -> NAddress.parse(bytes)
131 | // "nembed1" -> NEmbed.parse(bytes)
132 | else -> null
133 | }?.let {
134 | ParseReturn(it, nip19, additionalChars)
135 | }
136 | } catch (e: Throwable) {
137 | serviceLogger.e("NIP19 Parser: Issue trying to Decode NIP19 $key: ${e.message}", e)
138 | null
139 | }
140 |
141 | fun parseAll(content: String): List {
142 | val matcher2 = nip19regex.findAll(content)
143 | val returningList = mutableListOf()
144 | matcher2.forEach { result ->
145 | val type = result.groups[2]?.value // npub1
146 | val key = result.groups[3]?.value // bech32
147 | val additionalChars = result.groups[4]?.value // additional chars
148 |
149 | if (type != null) {
150 | val parsed = parseComponents(type, key, additionalChars)?.entity
151 |
152 | if (parsed != null) {
153 | returningList.add(parsed)
154 | }
155 | }
156 | }
157 | return returningList
158 | }
159 | }
160 |
161 | fun decodePublicKey(key: String): ByteArray =
162 | when (val parsed = Nip19Parser.parse(key)?.entity) {
163 | is NSec -> Identity(privKey = key.bechToBytes()).pubKey
164 | is NPub -> parsed.hex.toBytes()
165 | is NProfile -> parsed.hex.toBytes()
166 | else -> Hex.decode(key) // crashes on purpose
167 | }
168 |
169 | fun decodePrivateKeyAsHexOrNull(key: String): String? =
170 | try {
171 | when (val parsed = Nip19Parser.parse(key)?.entity) {
172 | is NSec -> parsed.hex
173 | is NPub -> null
174 | is NProfile -> null
175 | is Note -> null
176 | is NEvent -> null
177 | // is NEmbed -> null
178 | is NRelay -> null
179 | is NAddress -> null
180 | else -> Hex.decode(key).toHexString()
181 | }
182 | } catch (e: Exception) {
183 | if (e is CancellationException) throw e
184 | null
185 | }
186 |
187 | fun decodePublicKeyAsHexOrNull(key: String): String? =
188 | try {
189 | when (val parsed = Nip19Parser.parse(key)?.entity) {
190 | is NSec -> Identity(privKey = key.bechToBytes()).pubKey.toHexString()
191 | is NPub -> parsed.hex
192 | is NProfile -> parsed.hex
193 | is Note -> null
194 | is NEvent -> null
195 | // is NEmbed -> null
196 | is NRelay -> null
197 | is NAddress -> null
198 | else -> Hex.decode(key).toHexString()
199 | }
200 | } catch (e: Exception) {
201 | if (e is CancellationException) throw e
202 | null
203 | }
204 |
205 | fun decodeEventIdAsHexOrNull(key: String): String? =
206 | try {
207 | when (val parsed = Nip19Parser.parse(key)?.entity) {
208 | is NSec -> null
209 | is NPub -> null
210 | is NProfile -> null
211 | is Note -> parsed.hex
212 | is NEvent -> parsed.hex
213 | is NAddress -> parsed.aTag()
214 | // is NEmbed -> null
215 | is NRelay -> null
216 | else -> Hex.decode(key).toHexString()
217 | }
218 | } catch (e: Exception) {
219 | if (e is CancellationException) throw e
220 | null
221 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rhodium
2 |
3 | [](https://kotlinlang.org)
4 | [](https://search.maven.org/search?q=g:io.github.kotlingeekdev)
5 |
6 | 
7 | 
8 | 
9 | 
10 | 
11 |
12 | A Kotlin Multiplatform library for working with Nostr, with support for JVM, Android, Linux, MacOS/iOS.
13 |
14 | Note: This is still in development and very incomplete.
15 |
16 |
17 | ## What is Nostr?
18 | * An introduction or description of Nostr can be found [here](https://github.com/nostr-protocol/nostr).
19 | * The Nostr protocol specs can be found [here](https://github.com/nostr-protocol/nips).
20 |
21 | ## How to include the libary
22 | You can include the library from either Maven Central or Jitpack.
23 |
24 | ### Maven
25 | You can include the library in the common source set like this:
26 | ```kotlin
27 | dependencies {
28 | implementation("io.github.kotlingeekdev:rhodium:1.0-beta-19")
29 |
30 | }
31 | ```
32 |
33 | ### Jitpack
34 | Inside your root-level `build.gradle(.kts)` file, you should add `jitpack`:
35 | ``` kotlin
36 | // build.gradle.kts
37 | allprojects {
38 | repositories {
39 | // ...
40 | maven { setUrl("https://jitpack.io") }
41 | }
42 | // ...
43 | }
44 | ```
45 |
46 | or
47 |
48 | ``` groovy
49 | // build.gradle
50 | allprojects {
51 | repositories {
52 | // ...
53 | maven { url "https://jitpack.io" }
54 | }
55 | // ...
56 | }
57 | ```
58 |
59 | In newer projects, you need to also update the `settings.gradle(.kts)` file's `dependencyResolutionManagement` block:
60 |
61 | ```
62 | dependencyResolutionManagement {
63 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
64 | repositories {
65 | google()
66 | mavenCentral()
67 | maven { url 'https://jitpack.io' } // <--
68 | jcenter() // Warning: this repository is going to shut down soon
69 | }
70 | }
71 | ```
72 | then, in your module's `build.gradle(.kts)`, you need to add:
73 | ```kotlin
74 | // build.gradle.kts
75 | dependencies {
76 | //...
77 | implementation("com.github.KotlinGeekDev.Rhodium:rhodium:1.0-beta-19")
78 |
79 |
80 | }
81 |
82 | ```
83 | If you're including it in an Android app, you can just add:
84 | ```kotlin
85 | // app/build.gradle.kts
86 | dependencies {
87 | //...
88 | implementation("com.github.KotlinGeekDev.Rhodium:rhodium-android:1.0-beta-19")
89 |
90 | }
91 | ```
92 |
93 | ## Usage
94 | When publishing an event, or making a subscription/close request to a relay,
95 | [`ClientMessage`](rhodium-core/src/commonMain/kotlin/rhodium/nostr/client/ClientMessage.kt) is used to encode the request/event,
96 | and anything sent by a relay is encoded as a [`RelayMessage`](rhodium-core/src/commonMain/kotlin/rhodium/nostr/relay/RelayMessage.kt).
97 | Relays can be configured using a `RelayPool`,
98 | and actual communication with relays is done with the `NostrService`.
99 | You can setup the NostrService with/without a custom relay pool as follows:
100 | ```kotlin
101 | // With a custom relay pool.
102 | val requestRelays = RelayPool.fromUrls("wss://relay1", "wss://relay2")
103 | val clientService = NostrService(
104 | relayPool = requestRelays
105 | )
106 |
107 | //Without a custom pool(using the default pool)
108 | val service = NostrService()
109 | ```
110 | You can also use a custom HTTP client for the `NostrService`, as long as it has Websocket support.
111 | ```kotlin
112 | val myHttpClient = httpClient {
113 | // custom configs(or not)
114 | }
115 | val service = NostrService(customClient = myHttpClient)
116 | ```
117 | The current limitation is that the HTTP client needs to be Ktor-compatible, that is, you create a
118 | custom Ktor engine that uses your client underneath.
119 |
120 | Note that if you need to do anything custom, such as using read-only relays,
121 | you will need to setup the list of relays, then use them in the relay pool:
122 | ```kotlin
123 | val customRelays = listOf(
124 | Relay("wss://relay1", readPolicy = true, writePolicy = false), // <-- A relay with custom read/write policy.
125 | Relay("wss://relay2"),
126 | )
127 |
128 | val customPool = RelayPool(relays = customRelays)
129 | ```
130 | ### Making a subscription request
131 | In order to make a subscription request, you need to construct a `RequestMessage`.
132 | And to do that, you need to pass in a subscriptionId(just a string), and a `NostrFilter`:
133 | ```kotlin
134 | val postsCommentsByFiatjafFilter = NostrFilter.newFilter()
135 | .kinds(EventKind.TEXT_NOTE.kind, EventKind.COMMENT.kind) // <-- Looking for posts and comments. Other kinds can be added
136 | .authors("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d") // <-- The profiles for which we want to find the posts and comments(as indicated by .kinds() above)
137 | .limit(20) // <-- Setting a limit is important, to avoid issues with relays
138 | .build()
139 |
140 | val myRequest = RequestMessage(
141 | subscriptionId = "fiatjaf_posts_and_comments",
142 | filters = listOf(postsCommentsByFiatjafFilter)
143 | )
144 | ```
145 |
146 | If you want to make a request with a single filter, you can do so with `RequestMessage.singleFilterRequest`.
147 | Using the same example above, we can rewrite it as:
148 |
149 | ```kotlin
150 | val postsCommentsByFiatjafFilter = ... //Same as above
151 |
152 | val myRequest = RequestMessage.singleFilterRequest( // <- singleFilterRequest is called here.
153 | subscriptionId = "fiatjaf_posts_and_comments",
154 | filter = postsByFiatjafFilter
155 | )
156 | ```
157 |
158 | **Note**: If you just want to send the request or event JSON directly, you can use `NostrService.sendRaw`.
159 | It is a `suspend` function as well.
160 |
161 | Now, you can use the `NostrService` to make the request, either using `request()` or `requestWithResult()`.
162 | They are both `suspend` functions, and as such, should be called within the appropriate context.
163 | **Note**: `requestWithResult` terminates the connection(s) after receiving all the expected messages. This behaviour could be
164 | modified in the future.
165 |
166 | The `request()` function has a callback used for handling incoming messages,
167 | `onRelayMessage: suspend (Relay, RelayMessage)`, as well as a callback for handling errors, `onRequestError(Relay, Throwable)`.
168 | An example is given below:
169 | ```kotlin
170 | // Example coroutine scope
171 | val appScope = CoroutineScope(Dispatchers.IO)
172 | //Using request() --
173 | appScope.launch {
174 | clientService.request(
175 | myRequest,
176 | onRequestError = { relay, throwable -> handleError(relay, throwable) }
177 | ) { relay: Relay, received: RelayMessage -> // This is a suspend callback, so suspend functions can be used here.
178 | useMessage(relay, received)
179 | }
180 | }
181 | ```
182 | The `requestWithResult()` function returns a list of events(`List`), no matter the errors that occur
183 | during its execution. The function however displays the errors using standard output. This may be changed in the future.
184 | ```kotlin
185 | //Using requestWithResult() ---
186 | // - Assuming a suspending context:
187 | val events = clientService.requestWithResult(myRequest)
188 | events.forEach { println(it.content) }
189 | // OR, following with the request() example above:
190 | appScope.launch {
191 | val events = clientService.requestWithResult(myRequest)
192 | events.forEach { println(it.content) }
193 | }
194 | ```
195 | ## License
196 |
197 | MIT License
198 |
199 | Copyright (c) 2025 KotlinGeekDev
200 |
201 | Permission is hereby granted, free of charge, to any person obtaining a copy
202 | of this software and associated documentation files (the "Software"), to deal
203 | in the Software without restriction, including without limitation the rights
204 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
205 | copies of the Software, and to permit persons to whom the Software is
206 | furnished to do so, subject to the following conditions:
207 |
208 | The above copyright notice and this permission notice shall be included in all
209 | copies or substantial portions of the Software.
210 |
211 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
212 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
213 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
214 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
215 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
216 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
217 | SOFTWARE.
218 |
--------------------------------------------------------------------------------
/rhodium-core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2025 KotlinGeekDev
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | *
24 | */
25 |
26 |
27 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
28 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
29 | import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest
30 | import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile
31 | import java.io.ByteArrayOutputStream
32 |
33 | val coroutinesVersion = "1.10.1"
34 | val kotlinVersion = "2.1.10"
35 | val ktorVersion = "3.1.1"
36 | val kotlinCryptoVersion = "0.4.0"
37 | val secp256k1Version = "0.17.1"
38 | val junitJupiterVersion = "5.10.1"
39 |
40 | plugins {
41 | kotlin("multiplatform")
42 | id("com.android.library")
43 | kotlin("plugin.serialization")
44 | }
45 |
46 | android {
47 | namespace = "io.github.kotlingeekdev.rhodium.android"
48 | compileSdk = 34
49 | defaultConfig {
50 | minSdk = 21
51 | targetSdk = 34
52 | compileSdk = 34
53 |
54 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
55 | consumerProguardFiles("consumer-rules.pro")
56 | }
57 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
58 |
59 | compileOptions {
60 | isCoreLibraryDesugaringEnabled = false
61 | sourceCompatibility = JavaVersion.VERSION_17
62 | targetCompatibility = JavaVersion.VERSION_17
63 | }
64 |
65 | }
66 |
67 |
68 | kotlin {
69 | //explicitApi()
70 | jvmToolchain(17)
71 |
72 | // @OptIn(ExperimentalKotlinGradlePluginApi::class)
73 | // compilerOptions {
74 | // apiVersion.set(KotlinVersion.KOTLIN_2_0)
75 | // languageVersion.set(KotlinVersion.KOTLIN_2_0)
76 | // }
77 |
78 | jvm("commonJvm") {
79 |
80 | @OptIn(ExperimentalKotlinGradlePluginApi::class)
81 | compilerOptions.jvmTarget.set(JvmTarget.JVM_17)
82 |
83 |
84 | testRuns["test"].executionTask.configure {
85 | useJUnitPlatform()
86 | testLogging {
87 | events("passed", "skipped", "failed")
88 | }
89 | }
90 | }
91 |
92 |
93 | androidTarget() {
94 |
95 | publishAllLibraryVariants()
96 | @OptIn(ExperimentalKotlinGradlePluginApi::class)
97 | compilerOptions {
98 | jvmTarget.set(JvmTarget.JVM_1_8)
99 | }
100 | }
101 |
102 |
103 | linuxX64("linux") {
104 | // compilations.all {
105 | // cinterops {
106 | // val libs by creating {
107 | // defFile("src/linuxMain/cinterop/libs.def")
108 | // }
109 | // }
110 | // }
111 | //
112 | // binaries {
113 | // sharedLib {
114 | //
115 | // }
116 | // executable {
117 | // entryPoint = "main"
118 | // }
119 | // }
120 | }
121 |
122 | //Apple targets
123 | val macosX64 = macosX64()
124 | val macosArm64 = macosArm64()
125 | val iosArm64 = iosArm64()
126 | val iosX64 = iosX64()
127 | val iosSimulatorArm64 = iosSimulatorArm64()
128 | val appleTargets = listOf(
129 | macosX64, macosArm64,
130 | iosArm64, iosX64, iosSimulatorArm64,
131 | )
132 |
133 | appleTargets.forEach { target ->
134 | with(target) {
135 | binaries {
136 | framework {
137 | baseName = "Rhodium"
138 | }
139 | }
140 | }
141 | }
142 |
143 |
144 | applyDefaultHierarchyTemplate()
145 |
146 | sourceSets {
147 | commonMain.dependencies {
148 | //Ktor
149 | implementation("io.ktor:ktor-client-core:$ktorVersion")
150 | implementation("io.ktor:ktor-client-websockets:$ktorVersion")
151 | implementation("io.ktor:ktor-client-logging:$ktorVersion")
152 |
153 | //Kotlin base
154 | implementation("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
155 | implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
156 |
157 | //Crypto(Secp256k1-utils, SecureRandom, Hashing, etc.)
158 | implementation("fr.acinq.secp256k1:secp256k1-kmp:$secp256k1Version")
159 | implementation("dev.whyoleg.cryptography:cryptography-core:$kotlinCryptoVersion")
160 | implementation("dev.whyoleg.cryptography:cryptography-random:$kotlinCryptoVersion")
161 |
162 | //Serialization
163 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
164 | //Coroutines
165 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
166 | //Atomics
167 | implementation("org.jetbrains.kotlinx:atomicfu:0.27.0")
168 | //Date-time
169 | implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
170 | //UUID
171 | implementation("com.benasher44:uuid:0.8.4")
172 | //ByteBuffer(until a kotlinx-io replacement appears)
173 | implementation("com.ditchoom:buffer:1.4.2")
174 | //Logging
175 | implementation("co.touchlab:kermit:2.0.5")
176 | }
177 |
178 | commonTest.dependencies {
179 | implementation(kotlin("test-common"))
180 | implementation(kotlin("test-annotations-common"))
181 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
182 | }
183 |
184 | val commonJvmMain by getting {
185 |
186 | dependencies {
187 | implementation("dev.whyoleg.cryptography:cryptography-provider-jdk:$kotlinCryptoVersion")
188 |
189 | implementation("com.squareup.okhttp3:okhttp:4.12.0")
190 | implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
191 | //implementation("fr.acinq.secp256k1:secp256k1-kmp-jvm:0.6.4")
192 | implementation("fr.acinq.secp256k1:secp256k1-kmp-jni-jvm:$secp256k1Version")
193 | }
194 | }
195 |
196 | val commonJvmTest by getting {
197 |
198 | dependencies {
199 | implementation(kotlin("test-junit5"))
200 |
201 | implementation("org.junit.jupiter:junit-jupiter:$junitJupiterVersion")
202 | implementation("org.junit.jupiter:junit-jupiter-params:$junitJupiterVersion")
203 | implementation("org.assertj:assertj-core:3.23.1")
204 | runtimeOnly("fr.acinq.secp256k1:secp256k1-kmp-jni-jvm-linux:$secp256k1Version")
205 | runtimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion")
206 | runtimeOnly("org.junit.vintage:junit-vintage-engine:$junitJupiterVersion")
207 | }
208 | }
209 |
210 |
211 |
212 | androidMain.configure {
213 |
214 | dependencies {
215 | implementation("androidx.appcompat:appcompat:1.7.0")
216 | // coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.3")
217 | implementation("dev.whyoleg.cryptography:cryptography-provider-jdk:$kotlinCryptoVersion")
218 | implementation("com.squareup.okhttp3:okhttp:4.12.0")
219 | implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
220 | implementation("fr.acinq.secp256k1:secp256k1-kmp-jni-android:$secp256k1Version")
221 | }
222 | }
223 |
224 | androidUnitTest.configure {
225 | dependsOn(commonJvmTest)
226 | dependencies {
227 | implementation("junit:junit:4.13.2")
228 | }
229 | }
230 |
231 | androidInstrumentedTest.configure {
232 | dependsOn(commonJvmTest)
233 | dependencies {
234 | implementation("androidx.test.ext:junit:1.2.1")
235 | implementation("androidx.test.espresso:espresso-core:3.6.1")
236 | }
237 | }
238 |
239 | linuxMain.configure {
240 | dependencies {
241 | // implementation("io.ktor:ktor-client-cio:$ktorVersion")
242 | implementation("io.ktor:ktor-client-curl:$ktorVersion")
243 | implementation("dev.whyoleg.cryptography:cryptography-provider-openssl3-prebuilt:$kotlinCryptoVersion")
244 | }
245 | }
246 |
247 | linuxTest.configure {
248 | dependencies {
249 |
250 | }
251 |
252 | }
253 |
254 | appleMain.configure {
255 | dependsOn(commonMain.get())
256 | dependencies {
257 | implementation("io.ktor:ktor-client-darwin:$ktorVersion")
258 | implementation("dev.whyoleg.cryptography:cryptography-provider-apple:$kotlinCryptoVersion")
259 | }
260 | }
261 | appleTest.configure {
262 | dependsOn(commonTest.get())
263 | }
264 |
265 | appleTargets.forEach { target ->
266 | getByName("${target.targetName}Main") { dependsOn(appleMain.get()) }
267 | getByName("${target.targetName}Test") { dependsOn(appleTest.get()) }
268 | }
269 |
270 | }
271 | }
272 |
273 |
274 | tasks.withType() {
275 | compilerOptions {
276 | jvmTarget = JvmTarget.JVM_17
277 | }
278 | }
279 |
280 | tasks.withType().configureEach {
281 | compilerOptions.freeCompilerArgs.add("-opt-in=kotlinx.cinterop.ExperimentalForeignApi")
282 | }
283 |
284 |
285 | val deviceName = project.findProperty("iosDevice") as? String ?: "iPhone 16"
286 |
287 | tasks.register("bootIOSSimulator") {
288 | isIgnoreExitValue = true
289 | val errorBuffer = ByteArrayOutputStream()
290 | errorOutput = ByteArrayOutputStream()
291 | commandLine("xcrun", "simctl", "boot", deviceName)
292 |
293 | doLast {
294 | val result = executionResult.get()
295 | if (result.exitValue != 148 && result.exitValue != 149) { // ignoring device already booted errors
296 | println(errorBuffer.toString())
297 | result.assertNormalExitValue()
298 | }
299 | }
300 | }
301 |
302 | tasks.withType().configureEach {
303 | dependsOn("bootIOSSimulator")
304 | standalone.set(false)
305 | device.set(deviceName)
306 |
307 | }
308 |
309 | tasks.register("shutdownSimulator") {
310 |
311 | val allSimulatorTests = tasks.withType()
312 | if (allSimulatorTests.all { task -> task.state.failure == null }) {
313 | commandLine("xcrun", "simctl", "shutdown", "booted")
314 | }
315 |
316 | }
317 |
318 | tasks.named { it.contains("ios") && it.contains("Test") }.configureEach {
319 | finalizedBy("shutdownSimulator")
320 | }
321 |
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/crypto/Bech32Util.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2022 KotlinGeekDev
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy
5 | * of this software and associated documentation files (the "Software"), to deal
6 | * in the Software without restriction, including without limitation the rights
7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | * copies of the Software, and to permit persons to whom the Software is
9 | * furnished to do so, subject to the following conditions:
10 | *
11 | * The above copyright notice and this permission notice shall be included in all
12 | * copies or substantial portions of the Software.
13 | *
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | * SOFTWARE.
21 | *
22 | */
23 |
24 | package rhodium.crypto
25 |
26 | /**
27 | * Copyright (c) 2024 Vitor Pamplona
28 | *
29 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
30 | * this software and associated documentation files (the "Software"), to deal in
31 | * the Software without restriction, including without limitation the rights to use,
32 | * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
33 | * Software, and to permit persons to whom the Software is furnished to do so,
34 | * subject to the following conditions:
35 | *
36 | * The above copyright notice and this permission notice shall be included in all
37 | * copies or substantial portions of the Software.
38 | *
39 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
41 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
42 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
43 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
44 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45 | */
46 | ///////////////////////////////////////////////////////////////////////////////////
47 | /*
48 | * Copyright 2020 ACINQ SAS
49 | *
50 | * Licensed under the Apache License, Version 2.0 (the "License");
51 | * you may not use this file except in compliance with the License.
52 | * You may obtain a copy of the License at
53 | *
54 | * http://www.apache.org/licenses/LICENSE-2.0
55 | *
56 | * Unless required by applicable law or agreed to in writing, software
57 | * distributed under the License is distributed on an "AS IS" BASIS,
58 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
59 | * See the License for the specific language governing permissions and
60 | * limitations under the License.
61 | */
62 | import kotlin.jvm.JvmStatic
63 | /**
64 | * Bech32 works with 5 bits values, we use this type to make it explicit: whenever you see Int5 it
65 | * means 5 bits values, and whenever you see Byte it means 8 bits values.
66 | */
67 | private typealias Int5 = Byte
68 |
69 | /**
70 | * Bech32 and Bech32m address formats. See
71 | * https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki and
72 | * https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki.
73 | */
74 | object Bech32 {
75 | const val ALPHABET: String = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
76 | const val ALPHABET_UPPERCASE: String = "QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L"
77 |
78 | enum class Encoding(
79 | val constant: Int,
80 | ) {
81 | Bech32(1),
82 | Bech32m(0x2bc830a3),
83 | Beck32WithoutChecksum(0),
84 | }
85 |
86 | // char -> 5 bits value
87 | private val map = Array(255) { -1 }
88 |
89 | init {
90 | for (i in 0..ALPHABET.lastIndex) {
91 | map[ALPHABET[i].code] = i.toByte()
92 | }
93 | for (i in 0..ALPHABET_UPPERCASE.lastIndex) {
94 | map[ALPHABET_UPPERCASE[i].code] = i.toByte()
95 | }
96 | }
97 |
98 | fun expand(hrp: String): Array {
99 | val half = hrp.length + 1
100 | val size = half + hrp.length
101 | return Array(size) {
102 | when (it) {
103 | in hrp.indices -> hrp[it].code.shr(5).toByte()
104 | in half until size -> (hrp[it - half].code and 31).toByte()
105 | else -> 0
106 | }
107 | }
108 | }
109 |
110 | private val GEN = arrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
111 |
112 | fun polymod(
113 | values: Array,
114 | values1: Array,
115 | ): Int {
116 | var chk = 1
117 | values.forEach { v ->
118 | val b = chk shr 25
119 | chk = ((chk and 0x1ffffff) shl 5) xor v.toInt()
120 | for (i in 0..4) {
121 | if (((b shr i) and 1) != 0) chk = chk xor GEN[i]
122 | }
123 | }
124 | values1.forEach { v ->
125 | val b = chk shr 25
126 | chk = ((chk and 0x1ffffff) shl 5) xor v.toInt()
127 | for (i in 0..4) {
128 | if (((b shr i) and 1) != 0) chk = chk xor GEN[i]
129 | }
130 | }
131 | return chk
132 | }
133 |
134 | /**
135 | * @param hrp human readable prefix
136 | * @param int5s 5-bit data
137 | * @param encoding encoding to use (bech32 or bech32m)
138 | * @return hrp + data encoded as a Bech32 string
139 | */
140 | @JvmStatic
141 | public fun encode(
142 | hrp: String,
143 | int5s: ArrayList,
144 | encoding: Encoding,
145 | ): String {
146 | require(hrp.lowercase() == hrp || hrp.uppercase() == hrp) {
147 | "mixed case strings are not valid bech32 prefixes"
148 | }
149 | val dataWithChecksum =
150 | when (encoding) {
151 | Encoding.Beck32WithoutChecksum -> int5s
152 | else -> addChecksum(hrp, int5s, encoding)
153 | }
154 |
155 | val charArray =
156 | CharArray(dataWithChecksum.size) { ALPHABET[dataWithChecksum[it].toInt()] }.concatToString()
157 |
158 | return hrp + "1" + charArray
159 | }
160 |
161 | /**
162 | * @param hrp human readable prefix
163 | * @param data data to encode
164 | * @param encoding encoding to use (bech32 or bech32m)
165 | * @return hrp + data encoded as a Bech32 string
166 | */
167 | @JvmStatic
168 | public fun encodeBytes(
169 | hrp: String,
170 | data: ByteArray,
171 | encoding: Encoding,
172 | ): String = encode(hrp, eight2five(data), encoding)
173 |
174 | /**
175 | * decodes a bech32 string
176 | *
177 | * @param bech32 bech32 string
178 | * @param noChecksum if true, the bech32 string doesn't have a checksum
179 | * @return a (hrp, data, encoding) tuple
180 | */
181 | @JvmStatic
182 | public fun decode(
183 | bech32: String,
184 | noChecksum: Boolean = false,
185 | ): Triple, Encoding> {
186 | var pos = 0
187 | bech32.forEachIndexed { index, char ->
188 | require(char.code in 33..126) { "invalid character $char" }
189 | if (char == '1') {
190 | pos = index
191 | }
192 | }
193 |
194 | val hrp = bech32.take(pos).lowercase() // strings must be lower case
195 | require(hrp.length in 1..83) { "hrp must contain 1 to 83 characters" }
196 |
197 | val data = Array(bech32.length - pos - 1) { map[bech32[pos + 1 + it].code] }
198 |
199 | return if (noChecksum) {
200 | Triple(hrp, data, Encoding.Beck32WithoutChecksum)
201 | } else {
202 | val encoding =
203 | when (polymod(expand(hrp), data)) {
204 | Encoding.Bech32.constant -> Encoding.Bech32
205 | Encoding.Bech32m.constant -> Encoding.Bech32m
206 | else -> throw IllegalArgumentException("invalid checksum for $bech32")
207 | }
208 | Triple(hrp, data.copyOfRange(0, data.size - 6), encoding)
209 | }
210 | }
211 |
212 | /**
213 | * decodes a bech32 string
214 | *
215 | * @param bech32 bech32 string
216 | * @param noChecksum if true, the bech32 string doesn't have a checksum
217 | * @return a (hrp, data, encoding) tuple
218 | */
219 | @JvmStatic
220 | public fun decodeBytes(
221 | bech32: String,
222 | noChecksum: Boolean = false,
223 | ): Triple {
224 | val (hrp, int5s, encoding) = decode(bech32, noChecksum)
225 | return Triple(hrp, five2eight(int5s, 0), encoding)
226 | }
227 |
228 | val ZEROS = arrayOf(0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte())
229 |
230 | /**
231 | * @param hrp Human Readable Part
232 | * @param data data (a sequence of 5 bits integers)
233 | * @param encoding encoding to use (bech32 or bech32m)
234 | * @return a checksum computed over hrp and data
235 | */
236 | private fun addChecksum(
237 | hrp: String,
238 | data: ArrayList,
239 | encoding: Encoding,
240 | ): ArrayList {
241 | val values = expand(hrp) + data
242 | val poly = polymod(values, ZEROS) xor encoding.constant
243 |
244 | for (i in 0 until 6) {
245 | data.add((poly.shr(5 * (5 - i)) and 31).toByte())
246 | }
247 |
248 | return data
249 | }
250 |
251 | /**
252 | * @param input a sequence of 8 bits integers
253 | * @return a sequence of 5 bits integers
254 | */
255 | @JvmStatic
256 | public fun eight2five(input: ByteArray): ArrayList {
257 | var buffer = 0L
258 | val output =
259 | ArrayList(input.size * 2) // larger array on purpose. Checksum is added later.
260 | var count = 0
261 | input.forEach { b ->
262 | buffer = (buffer shl 8) or (b.toLong() and 0xff)
263 | count += 8
264 | while (count >= 5) {
265 | output.add(((buffer shr (count - 5)) and 31).toByte())
266 | count -= 5
267 | }
268 | }
269 | if (count > 0) output.add(((buffer shl (5 - count)) and 31).toByte())
270 | return output
271 | }
272 |
273 | /**
274 | * @param input a sequence of 5 bits integers
275 | * @return a sequence of 8 bits integers
276 | */
277 | @JvmStatic
278 | public fun five2eight(
279 | input: Array,
280 | offset: Int,
281 | ): ByteArray {
282 | var buffer = 0L
283 | val output = ArrayList(input.size)
284 | var count = 0
285 | for (i in offset..input.lastIndex) {
286 | val b = input[i]
287 | buffer = (buffer shl 5) or (b.toLong() and 31)
288 | count += 5
289 | while (count >= 8) {
290 | output.add(((buffer shr (count - 8)) and 0xff).toByte())
291 | count -= 8
292 | }
293 | }
294 | require(count <= 4) { "Zero-padding of more than 4 bits" }
295 | require((buffer and ((1L shl count) - 1L)) == 0L) { "Non-zero padding in 8-to-5 conversion" }
296 | return output.toByteArray()
297 | }
298 | }
299 |
300 | fun String.bechToBytes(hrp: String? = null): ByteArray {
301 | val decodedForm = Bech32.decodeBytes(this)
302 | hrp?.also {
303 | if (it != decodedForm.first) {
304 | throw IllegalArgumentException("Expected $it but obtained ${decodedForm.first}")
305 | }
306 | }
307 | return decodedForm.second
308 | }
--------------------------------------------------------------------------------
/rhodium-core/src/commonMain/kotlin/rhodium/nostr/relay/Relay.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2025 KotlinGeekDev
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | *
24 | */
25 |
26 | package rhodium.nostr.relay
27 |
28 | import io.ktor.client.HttpClient
29 | import io.ktor.client.request.get
30 | import io.ktor.client.statement.bodyAsText
31 | import io.ktor.http.isSuccess
32 | import kotlinx.serialization.SerialName
33 | import kotlinx.serialization.Serializable
34 | import rhodium.net.httpClient
35 | import rhodium.nostr.RelayInfoFetchError
36 | import rhodium.nostr.eventMapper
37 | import rhodium.nostr.relay.info.Payments
38 | import rhodium.nostr.relay.info.RelayLimits
39 | import rhodium.nostr.relay.info.RetentionPolicy
40 |
41 | /**
42 | * Represents a Nostr relay, including it's read/write policies.
43 | * When creating you can use the `Relay(...)` constructor, or use
44 | * [fromUrl], and just pass in the relay address.
45 | *
46 | * The read/write policies are set to true by default.
47 | *
48 | * @constructor Relay(String, Boolean, Boolean)
49 | *
50 | * @property relayURI - The relay's address, as a String
51 | * @property readPolicy - The relay's read status(whether a relay accepts reads from it), as a Boolean.
52 | * @property writePolicy - The relay's write status(whether a relay accepts writes to it), as a Boolean.
53 | */
54 | class Relay(
55 | val relayURI: String,
56 | val readPolicy: Boolean = true,
57 | val writePolicy: Boolean = true
58 | ) {
59 | companion object {
60 | fun fromUrl(address: String): Relay {
61 | return Relay(address)
62 | }
63 |
64 | /**
65 | * Fetches information about a relay, per [NIP-11](https://github.com/nostr-protocol/nips/blob/master/11.md),
66 | * and returns the information as an [Info] object.
67 | *
68 | * @param relayUrl - The relay URL/URI, as a String
69 | * @param httpClient - (Optional) A custom HTTP client that can be used for fetching the info,
70 | * particularly for a project using a particular client.
71 | * By default, the library's own [client][rhodium.net.httpClient] is used.
72 | *
73 | * @return Relay information, as an [Info] object.
74 | */
75 | suspend fun fetchInfoFor(
76 | relayUrl: String,
77 | httpClient: HttpClient = httpClient()
78 | ): Info {
79 | val raw = relayUrl.removePrefix("wss://").removePrefix("ws://")
80 | val actualUrl = "https://$raw"
81 | val relayInfoResponse = httpClient.get(actualUrl) {
82 | headers.append("Accept", "application/nostr+json")
83 | }
84 |
85 | if (relayInfoResponse.status.isSuccess()) {
86 | return infoFromJson(relayInfoResponse.bodyAsText())
87 | }
88 | else throw RelayInfoFetchError("Could not fetch relay info with reason: ${relayInfoResponse.bodyAsText()}")
89 | }
90 |
91 | /**
92 | * Transforms already obtained relay info in JSON form,
93 | * and returns it as an `Info` object.
94 | *
95 | * @param relayInfoJson - The relay info, in JSON format.
96 | *
97 | * @return Relay information, stored as an [Info] object.
98 | */
99 | fun infoFromJson(
100 | relayInfoJson: String
101 | ): Info {
102 | val relayInfo = eventMapper.decodeFromString(relayInfoJson)
103 | return relayInfo
104 | }
105 |
106 | /**
107 | * Does the exact opposite of [infoFromJson],
108 | * taking relay information stored in an `Info` object, and returns it in JSON format.
109 | *
110 | * @param relayInfo - The relay's information, as an `Info` object
111 | *
112 | * @return A JSON formatted `String` representation of the relay info.
113 | */
114 | fun infoToJson(
115 | relayInfo: Info
116 | ): String {
117 | val relayInfoJson = eventMapper.encodeToString(relayInfo)
118 | return relayInfoJson
119 | }
120 | }
121 |
122 | override fun toString(): String {
123 | return "Relay(url=$relayURI, read=$readPolicy, write=$writePolicy)"
124 | }
125 |
126 | /**
127 | * Represents a relay's information,
128 | * provided as per [NIP-11](https://github.com/nostr-protocol/nips/blob/master/11.md).
129 | * The elements required for a relay's details are its name, description, banner, icon,
130 | * pubkey, and contact. The other properties are optional.
131 | *
132 | * By default, an `Info` object is "empty".
133 | *
134 | * @property name - The relay's name
135 | * @property description - A description of the relay
136 | * @property banner - A banner for the relay, similar to a user profile's banner.
137 | * @property icon - An icon for the relay, similar to a user's profile picture.
138 | * @property pubkey - The Nostr pubkey of the relay's administrator.
139 | * @property contact - An alternative contact for the relay administrator.
140 | * @property supportedNips - (Optional) A list of Nostr specs(NIPs) supported by the relay.
141 | * @property relaySoftware - (Optional) The name of the underlying relay implementation used by this relay.
142 | * @property softwareVersion - (Optional) The version of the relay implementation currently in use by the relay.
143 | * @property privacyPolicy - (Optional) The privacy policy for this relay.
144 | * @property termsOfService - (Optional) The relay's terms of service.
145 | * @property limits - (Optional) A set of limits imposed by the relay, as a [RelayLimits] object.
146 | * @property retentionPolicies - (Optional) A set of data retention policies provided by the relay.
147 | * A retention policy is stored as a [RetentionPolicy] object.
148 | * @see RetentionPolicy
149 | *
150 | * @property relayRegionHosts - (Optional) Countries, regions, whose policies might affect the relay's hosted content.
151 | * @property allowedLanguages - (Optional) A relay's preference concerning the language of content to be published to it.
152 | * @property allowedTopics - (Optional) A set of allowed topics for discussion on this relay.
153 | * @property postingPolicy - (Optional) The relay's posting policy; a set of guidelines on content
154 | * to be published to the relay.
155 | * @property paymentUrl - (Optional) Indicates where you should pay the relay operator.
156 | * Usually for paid relays, or paid tiers on mixed relays.
157 | * @property paymentInfo - (Optional) Contains info on payments to make: what to pay for, how to pay, how much to pay, etc.
158 | * The information is stored as a [Payments] object.
159 | */
160 | @Serializable
161 | data class Info(
162 | val name: String = "",
163 | val description: String = "",
164 | val banner: String = "",
165 | val icon: String = "",
166 | val pubkey: String = "",
167 | val contact: String = "",
168 | @SerialName("supported_nips") val supportedNips: IntArray = emptyArray().toIntArray(),
169 | @SerialName("software") val relaySoftware: String = "",
170 | @SerialName("version") val softwareVersion: String = "",
171 | @SerialName("privacy_policy") val privacyPolicy: String = "",
172 | @SerialName("terms_of_service") val termsOfService: String = "",
173 | //Extra fields below
174 | @SerialName("limitation") val limits: RelayLimits? = null,
175 | @SerialName("retention") val retentionPolicies: Array? = null,
176 | //Extra field: Content Limitation
177 | @SerialName("relay_countries") val relayRegionHosts: Array? = null,
178 | //Extra field group: Community Preferences
179 | //TODO: Extract field group into separate class, and use custom serializer for Info.
180 | @SerialName("language_tags") val allowedLanguages: Array? = null,
181 | @SerialName("tags") val allowedTopics: Array? = null,
182 | @SerialName("posting_policy") val postingPolicy: String? = null,
183 | //Extra field group: Pay-to-Relay
184 | @SerialName("payments_url") val paymentUrl: String? = null,
185 | @SerialName("fees") val paymentInfo: Payments? = null
186 | ) {
187 | override fun equals(other: Any?): Boolean {
188 | if (this === other) return true
189 | if (other == null || this::class != other::class) return false
190 |
191 | other as Info
192 |
193 | if (name != other.name) return false
194 | if (description != other.description) return false
195 | if (banner != other.banner) return false
196 | if (icon != other.icon) return false
197 | if (pubkey != other.pubkey) return false
198 | if (contact != other.contact) return false
199 | if (!supportedNips.contentEquals(other.supportedNips)) return false
200 | if (relaySoftware != other.relaySoftware) return false
201 | if (softwareVersion != other.softwareVersion) return false
202 | if (limits != other.limits) return false
203 | if (!retentionPolicies.contentEquals(other.retentionPolicies)) return false
204 | if (!relayRegionHosts.contentEquals(other.relayRegionHosts)) return false
205 | if (!allowedLanguages.contentEquals(other.allowedLanguages)) return false
206 | if (!allowedTopics.contentEquals(other.allowedTopics)) return false
207 | if (postingPolicy != other.postingPolicy) return false
208 | if (paymentUrl != other.paymentUrl) return false
209 | if (paymentInfo != other.paymentInfo) return false
210 |
211 | return true
212 | }
213 |
214 | override fun hashCode(): Int {
215 | var result = name.hashCode()
216 | result = 31 * result + description.hashCode()
217 | result = 31 * result + banner.hashCode()
218 | result = 31 * result + icon.hashCode()
219 | result = 31 * result + pubkey.hashCode()
220 | result = 31 * result + contact.hashCode()
221 | result = 31 * result + supportedNips.contentHashCode()
222 | result = 31 * result + relaySoftware.hashCode()
223 | result = 31 * result + softwareVersion.hashCode()
224 | result = 31 * result + (limits?.hashCode() ?: 0)
225 | result = 31 * result + retentionPolicies.contentHashCode()
226 | result = 31 * result + (relayRegionHosts?.contentHashCode() ?: 0)
227 | result = 31 * result + (allowedLanguages?.contentHashCode() ?: 0)
228 | result = 31 * result + (allowedTopics?.contentHashCode() ?: 0)
229 | result = 31 * result + (postingPolicy?.hashCode() ?: 0)
230 | result = 31 * result + (paymentUrl?.hashCode() ?: 0)
231 | result = 31 * result + (paymentInfo?.hashCode() ?: 0)
232 | return result
233 | }
234 | }
235 | }
--------------------------------------------------------------------------------