├── 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 | [![Kotlin](https://img.shields.io/badge/Kotlin-2.0.20-blue?style=flat&logo=kotlin)](https://kotlinlang.org) 4 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.kotlingeekdev/rhodium?color=blue)](https://search.maven.org/search?q=g:io.github.kotlingeekdev) 5 | 6 | ![badge-jvm](http://img.shields.io/badge/platform-jvm-DB413D.svg?style=flat) 7 | ![badge-android](http://img.shields.io/badge/platform-android-6EDB8D.svg?style=flat) 8 | ![badge-linux](http://img.shields.io/badge/platform-linux-2D3F6C.svg?style=flat) 9 | ![badge-mac](http://img.shields.io/badge/platform-macos-111111.svg?style=flat) 10 | ![badge-ios](http://img.shields.io/badge/platform-ios-CDCDCD.svg?style=flat) 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 | } --------------------------------------------------------------------------------