Skip to content

Commit

Permalink
Add domain for user profile
Browse files Browse the repository at this point in the history
  • Loading branch information
HenrikJannsen committed Nov 17, 2024
1 parent d584cff commit fa8ffc3
Show file tree
Hide file tree
Showing 16 changed files with 480 additions and 2 deletions.
6 changes: 4 additions & 2 deletions bisqapps/androidNode/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@

import com.google.protobuf.gradle.proto
import org.apache.tools.ant.taskdefs.condition.Os
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.apache.tools.ant.taskdefs.condition.Os
import com.google.protobuf.gradle.proto

plugins {
alias(libs.plugins.kotlinMultiplatform)
Expand Down Expand Up @@ -161,6 +162,7 @@ dependencies {

implementation(libs.koin.core)
implementation(libs.koin.android)
implementation(libs.logging.kermit)
}

// ensure tests run on J17
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package network.bisq.mobile.android.node.domain.user_profile

import bisq.common.encoding.Hex
import bisq.security.DigestUtil
import bisq.security.SecurityService
import bisq.user.UserService
import bisq.user.identity.NymIdGenerator
import bisq.user.identity.UserIdentity
import co.touchlab.kermit.Logger
import network.bisq.mobile.domain.user_profile.UserProfileFacade
import network.bisq.mobile.domain.user_profile.UserProfileModel
import java.util.Random
import kotlin.math.max
import kotlin.math.min

/**
* This is a facade to the Bisq 2 libraries UserIdentityService and UserProfileServices.
* It provides the API for the users profile presenter to interact with that domain.
* It uses in a in-memory model for the relevant data required for the presenter to reflect the domains state.
* Persistence is done inside the Bisq 2 libraries.
*/
class NodeUserProfileFacade(
override val model: UserProfileModel,
val securityService: SecurityService,
val userService: UserService
) :
UserProfileFacade {

companion object {
private const val AVATAR_VERSION = 0
}

val log = Logger.withTag("NodeUserProfileFacade")


override fun hasUserProfile(): Boolean {
return userService.userIdentityService.userIdentities.isEmpty()
}

override suspend fun generateKeyPair() {
model as NodeUserProfileModel
val keyPair = securityService.keyBundleService.generateKeyPair()
model.keyPair = keyPair
val pubKeyHash = DigestUtil.hash(keyPair.public.encoded)
model.pubKeyHash = pubKeyHash
model.setId(Hex.encode(pubKeyHash))
val ts = System.currentTimeMillis()
val proofOfWork = userService.userIdentityService.mintNymProofOfWork(pubKeyHash)
val powDuration = System.currentTimeMillis() - ts
log.i("Proof of work creation completed after $powDuration ms")
createSimulatedDelay(powDuration)
model.proofOfWork = proofOfWork
val powSolution = proofOfWork.solution
val nym = NymIdGenerator.generate(pubKeyHash, powSolution)
model.setNym(nym)

// CatHash is in desktop, needs to be reimplemented or the javafx part extracted and refactored into a non javafx lib
// Image image = CatHash.getImage(pubKeyHash,
// powSolution,
// CURRENT_AVATARS_VERSION,
// CreateProfileModel.CAT_HASH_IMAGE_SIZE);
}

override suspend fun createAndPublishNewUserProfile() {
model as NodeUserProfileModel
model.setIsBusy(true) // UI should start busy animation based on that property
userService.userIdentityService.createAndPublishNewUserProfile(
model.nickName.value,
model.keyPair,
model.pubKeyHash,
model.proofOfWork,
AVATAR_VERSION,
"",
""
)
.whenComplete { userIdentity: UserIdentity?, throwable: Throwable? ->
// UI should stop busy animation and show `next` button
model.setIsBusy(false)
}
}

override fun findUserProfile(id: String): UserProfileModel? {
return getUserProfiles().find { model -> model.id.equals(id) }
}

override fun getUserProfiles(): Sequence<UserProfileModel> {
return userService.userIdentityService.userIdentities
.asSequence()
.map { userIdentity ->
val userProfile = userIdentity.userProfile
val model = NodeUserProfileModel()
model.setNickName(userProfile.nickName)
model.setNym(userProfile.nym)
model.setId(userProfile.id)
model.keyPair = userIdentity.identity.keyBundle.keyPair
model.pubKeyHash = userIdentity.userProfile.pubKeyHash
model.proofOfWork = userIdentity.userProfile.proofOfWork
model
}
}

private fun createSimulatedDelay(powDuration: Long) {
try {
// Proof of work creation for difficulty 65536 takes about 50 ms to 100 ms on a 4 GHz Intel Core i7.
// Target duration would be 500-2000 ms, but it is hard to find the right difficulty that works
// well also for low-end CPUs. So we take a rather safe lower difficulty value and add here some
// delay to not have a too fast flicker-effect in the UI when recreating the nym.
// We add a min delay of 200 ms with some randomness to make the usage of the proof of work more
// visible.
val random: Int = Random().nextInt(100)
// Limit to 200-2000 ms
Thread.sleep(
min(2000.0, max(200.0, (200 + random - powDuration).toDouble()))
.toLong()
)
} catch (e: InterruptedException) {
throw RuntimeException(e)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package network.bisq.mobile.android.node.domain.user_profile

import bisq.security.pow.ProofOfWork
import network.bisq.mobile.domain.user_profile.UserProfileModel
import java.security.KeyPair

class NodeUserProfileModel : UserProfileModel() {
lateinit var keyPair: KeyPair
lateinit var proofOfWork: ProofOfWork
}
27 changes: 27 additions & 0 deletions bisqapps/shared/domain/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinCocoapods)
alias(libs.plugins.androidLibrary)
kotlin("plugin.serialization") version "2.0.21"
}

version = project.findProperty("shared.version") as String
Expand Down Expand Up @@ -35,7 +36,33 @@ kotlin {
//put your multiplatform dependencies here
implementation(libs.koin.core)
implementation(libs.kotlinx.coroutines)
implementation(libs.logging.kermit)

implementation("io.ktor:ktor-client-core:3.0.1") {
exclude(group = "org.slf4j", module = "slf4j-api")
}
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3") {
exclude(group = "org.slf4j", module = "slf4j-api")
}
implementation("io.ktor:ktor-client-serialization:3.0.1") {
exclude(group = "org.slf4j", module = "slf4j-api")
}
implementation("io.ktor:ktor-client-json:3.0.1") {
exclude(group = "org.slf4j", module = "slf4j-api")
}
implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.1") {
exclude(group = "org.slf4j", module = "slf4j-api")
}
implementation("io.ktor:ktor-client-cio:3.0.1") {
exclude(group = "org.slf4j", module = "slf4j-api")
}
implementation("io.ktor:ktor-client-content-negotiation:3.0.1") {
exclude(group = "org.slf4j", module = "slf4j-api")
}
implementation("org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin:2.0.21") {
exclude(group = "org.slf4j", module = "slf4j-api")
}
implementation("com.squareup.okio:okio:3.9.1")
}
commonTest.dependencies {
implementation(libs.kotlin.test)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package network.bisq.mobile.client.service

import co.touchlab.kermit.Logger
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json

class ApiRequestService(val baseUrl: String) {
private val log = Logger.withTag("RequestService")

suspend fun get(path: String): String {
val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
}
return client.get(baseUrl + path).bodyAsText()
}

suspend fun post(path: String, requestBody: Any): String {
val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
}
return client.post(baseUrl + path) {
contentType(io.ktor.http.ContentType.Application.Json)
setBody(requestBody)
}.body()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package network.bisq.mobile.domain.client.main.user_profile

import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import network.bisq.mobile.client.user_profile.UserProfileResponse
import network.bisq.mobile.domain.user_profile.UserProfileFacade
import network.bisq.mobile.domain.user_profile.UserProfileModel

class ClientUserProfileFacade(
override val model: UserProfileModel,
private val apiGateway: UserProfileApiGateway
) :
UserProfileFacade {
private val log = Logger.withTag("IosClientUserProfileController")

// TODO Dispatchers.IO is not supported on iOS. Either customize or find whats on iOS appropriate.
private val coroutineScope = CoroutineScope(Dispatchers.Main)

override fun hasUserProfile(): Boolean {
TODO("Not yet implemented")
}

override suspend fun generateKeyPair() {
model as ClientUserProfileModel
coroutineScope.launch {
try {
val result = apiGateway.requestPreparedData()
model.preparedDataAsJson = result.first
val preparedData = result.second
model.keyPair = preparedData.keyPair
model.proofOfWork = preparedData.proofOfWork
model.setNym(preparedData.nym)
model.setId(preparedData.id)
} catch (e: Exception) {
log.e { e.toString() }
}
}
}

override suspend fun createAndPublishNewUserProfile() {
model as ClientUserProfileModel
coroutineScope.launch {
try {
val userProfileResponse: UserProfileResponse =
apiGateway.createAndPublishNewUserProfile(
model.nickName.value,
model.preparedDataAsJson
)
require(model.id.value == userProfileResponse.userProfileId)
{ "userProfileId from model does not match userProfileId from response" }
} catch (e: Exception) {
log.e { e.toString() }
}
}
}

override fun getUserProfiles(): Sequence<UserProfileModel> {
TODO("Not yet implemented")
}

override fun findUserProfile(id: String): UserProfileModel? {
TODO("Not yet implemented")
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package network.bisq.mobile.domain.client.main.user_profile

import network.bisq.mobile.domain.security.keys.KeyPair
import network.bisq.mobile.domain.security.pow.ProofOfWork
import network.bisq.mobile.domain.user_profile.UserProfileModel

class ClientUserProfileModel : UserProfileModel() {
lateinit var preparedDataAsJson: String
lateinit var keyPair: KeyPair
lateinit var proofOfWork: ProofOfWork
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package network.bisq.mobile.domain.client.main.user_profile

import kotlinx.serialization.Serializable

@Serializable
data class CreateUserIdentityRequest(
val nickName: String,
val terms: String = "",
val statement: String = "",
val preparedDataJson: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package network.bisq.mobile.domain.client.main.user_profile

import co.touchlab.kermit.Logger
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import network.bisq.mobile.client.service.ApiRequestService
import network.bisq.mobile.client.user_profile.UserProfileResponse
import network.bisq.mobile.domain.user.identity.PreparedData
import network.bisq.mobile.utils.ByteArrayAsBase64Serializer

class UserProfileApiGateway(
private val apiRequestService: ApiRequestService
) {
private val log = Logger.withTag("UserProfileApiGateway")

suspend fun requestPreparedData(): Pair<String, PreparedData> {
val response = apiRequestService.get("user-identity/prepared-data")
val json = Json {
serializersModule = SerializersModule {
contextual(ByteArrayAsBase64Serializer)
}
}
return Pair(response, json.decodeFromString<PreparedData>(response))
}

suspend fun createAndPublishNewUserProfile(
nickName: String,
preparedDataAsJson: String
): UserProfileResponse {
val createUserIdentityRequest = CreateUserIdentityRequest(
nickName,
"",
"",
preparedDataAsJson
)
val response =
apiRequestService.post("user-identity/user-identities", createUserIdentityRequest)
return Json.decodeFromString(response)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package network.bisq.mobile.client.user_profile

import kotlinx.serialization.Serializable

@Serializable
data class UserProfileResponse(val userProfileId: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package network.bisq.mobile.domain.security.keys

import kotlinx.serialization.Serializable

@Serializable
data class KeyPair(
val privateKey: String,
val publicKey: String
)
Loading

0 comments on commit fa8ffc3

Please sign in to comment.