Skip to content

Commit

Permalink
Add websocket support (#92)
Browse files Browse the repository at this point in the history
* Add websocket support

* Use backgroundScope instead of CoroutineScope(BackgroundDispatcher)

* Remove comment

* Make class thread safe.

* Use return triple instead of callback

* - fix crash on service init in iOS (coming from main)

* - move BuildConfig generation to domain module + move default networking values into gradle.properties

* - added networking setup to README docs for local dev

* - fix crash when connect fails

* - log timeouts on ws connection request handler

* - refactor: renaming to remove REST prefixes

* - rollback websocket api request/response naming cause the server expect that class naming

---------

Co-authored-by: Rodrigo Varela <[email protected]>
  • Loading branch information
HenrikJannsen and rodvar authored Dec 11, 2024
1 parent 5f508b1 commit c643e26
Show file tree
Hide file tree
Showing 63 changed files with 1,116 additions and 551 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- [Project dev requirements](#project-dev-requirements)
- [Getting started](#getting-started)
- [Getting started for Android Node](#getting-started-for-android-node)
- [Local Env Setup](#local-env-setup)
- [UI](#ui)
- [Designs](#designs)
- [Navigation Implementation](#navigation-implementation)
Expand Down Expand Up @@ -84,6 +85,24 @@ Addicionally, for the `androidNode` module to build you need to have its depende

Done! Alternatively if you are interested only in contributing for the `xClients` you can just build them individually instead of building the whole project.

### Local Env Setup

**Node**

You just need to run a local bisq seed node from the bisq2 project. By default port 8000 is used

**Clients**

You need to run the seed node as explained above + the http-api module with the following VM parameters

```
-Dapplication.appName=bisq2_restApi_clear
-Dapplication.network.supportedTransportTypes.2=CLEAR
-Dapplication.devMode=true
```

Default networking setup for the WebSocket (WS) connection can be found in `gradle.properties` file. You can change there for locally building pointing at the ip you are interested in.

### UI

**Designs**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import kotlinx.coroutines.flow.StateFlow
import network.bisq.mobile.android.node.AndroidApplicationService
import network.bisq.mobile.android.node.domain.offerbook.NodeOfferbookServiceFacade.Companion.toLibraryMarket
import network.bisq.mobile.android.node.domain.offerbook.NodeOfferbookServiceFacade.Companion.toReplicatedMarket
import network.bisq.mobile.domain.data.model.MarketListItem
import network.bisq.mobile.domain.data.model.MarketPriceItem
import network.bisq.mobile.domain.service.market_price.MarketPriceServiceFacade
import network.bisq.mobile.domain.data.model.MarketListItem
import network.bisq.mobile.utils.Logging

class NodeMarketPriceServiceFacade(private val applicationService: AndroidApplicationService.Provider) :
Expand All @@ -22,8 +22,11 @@ class NodeMarketPriceServiceFacade(private val applicationService: AndroidApplic
}

// Properties
private val _marketPriceItem = MutableStateFlow(MarketPriceItem.EMPTY)
override val marketPriceItem: StateFlow<MarketPriceItem> get() = _marketPriceItem
private val _selectedMarketPriceItem = MutableStateFlow(MarketPriceItem.EMPTY)
override val selectedMarketPriceItem: StateFlow<MarketPriceItem> get() = _selectedMarketPriceItem

private val _selectedFormattedMarketPrice = MutableStateFlow("N/A")
override val selectedFormattedMarketPrice: StateFlow<String> = _selectedFormattedMarketPrice

// Misc
private var selectedMarketPin: Pin? = null
Expand All @@ -49,27 +52,32 @@ class NodeMarketPriceServiceFacade(private val applicationService: AndroidApplic

// Private
private fun observeMarketPrice() {
marketPricePin = marketPriceService.marketPriceByCurrencyMap.addObserver { updatePrice() }
marketPricePin =
marketPriceService.marketPriceByCurrencyMap.addObserver { updateMarketPriceItem() }
}

private fun observeSelectedMarket() {
selectedMarketPin?.unbind()
selectedMarketPin = marketPriceService.selectedMarket.addObserver { market ->
try {
_marketPriceItem.value = MarketPriceItem(toReplicatedMarket(market))
updatePrice()
updateMarketPriceItem()
} catch (e: Exception) {
log.e("Failed to update market item", e)
}
}
}

private fun updatePrice() {
marketPriceService.findMarketPriceQuote(marketPriceService.selectedMarket.get())
.ifPresent { priceQuote ->
_marketPriceItem.value.setQuote(priceQuote.value)
val formattedPrice = PriceFormatter.format(priceQuote)
_marketPriceItem.value.setFormattedPrice(formattedPrice)
}
private fun updateMarketPriceItem() {
val market = marketPriceService.selectedMarket.get()
if (market != null) {
marketPriceService.findMarketPriceQuote(market)
.ifPresent { priceQuote ->
val replicatedMarket = toReplicatedMarket(market)
val formattedPrice = PriceFormatter.formatWithCode(priceQuote)
_selectedFormattedMarketPrice.value = formattedPrice
_selectedMarketPriceItem.value =
MarketPriceItem(replicatedMarket, priceQuote.value, formattedPrice)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import kotlinx.coroutines.flow.StateFlow
import network.bisq.mobile.android.node.AndroidApplicationService
import network.bisq.mobile.android.node.domain.offerbook.NodeOfferbookServiceFacade.Companion.toReplicatedMarket
import network.bisq.mobile.domain.LifeCycleAware
import network.bisq.mobile.domain.service.market_price.MarketPriceServiceFacade
import network.bisq.mobile.domain.data.model.OfferbookMarket
import network.bisq.mobile.domain.service.market_price.MarketPriceServiceFacade
import network.bisq.mobile.utils.Logging


Expand Down Expand Up @@ -92,7 +92,7 @@ class NodeSelectedOfferbookMarketService(
}

private fun updateMarketPrice() {
val formattedPrice = marketPriceServiceFacade.marketPriceItem.value.formattedPrice
_selectedOfferbookMarket.value.setFormattedPrice(formattedPrice.value)
val formattedPrice = marketPriceServiceFacade.selectedMarketPriceItem.value.formattedPrice
_selectedOfferbookMarket.value.setFormattedPrice(formattedPrice)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ class NodeOfferbookListItemService(private val applicationService: AndroidApplic
offerId,
isMyMessage,
direction,
market.quoteCurrencyCode,
offerTitle,
date,
formattedDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ class NodeUserProfileServiceFacade(private val applicationService: AndroidApplic
return userService.userIdentityService.userIdentities.map { userIdentity -> userIdentity.id }
}

override suspend fun applySelectedUserProfile(result: (String?, String?, String?) -> Unit) {
override suspend fun applySelectedUserProfile():Triple<String?, String?, String?> {
val userProfile = getSelectedUserProfile()
result(userProfile?.nickName, userProfile?.nym, userProfile?.id)
return Triple(userProfile?.nickName, userProfile?.nym, userProfile?.id)
}

// Private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,41 @@ class NodeMainPresenter(
private var applicationServiceCreated = false
override fun onViewAttached() {
super.onViewAttached()
if (!applicationServiceCreated) {
applicationServiceCreated = true
val filesDirsPath = (view as Activity).filesDir.toPath()
val applicationContext = (view as Activity).applicationContext
val applicationService =
AndroidApplicationService(
androidMemoryReportService,
applicationContext,
filesDirsPath
)
provider.applicationService = applicationService

applicationBootstrapFacade.activate()
log.i { "Start initializing applicationService" }
applicationService.initialize()
.whenComplete { r: Boolean?, throwable: Throwable? ->
if (throwable == null) {
log.i { "ApplicationService initialized" }
applicationBootstrapFacade.deactivate()
offerbookServiceFacade.activate()
marketPriceServiceFacade.activate()
} else {
log.e("Initializing applicationService failed", throwable)
runCatching {
if (!applicationServiceCreated) {
applicationServiceCreated = true
val filesDirsPath = (view as Activity).filesDir.toPath()
val applicationContext = (view as Activity).applicationContext
val applicationService =
AndroidApplicationService(
androidMemoryReportService,
applicationContext,
filesDirsPath
)
provider.applicationService = applicationService

applicationBootstrapFacade.activate()
log.i { "Start initializing applicationService" }
applicationService.initialize()
.whenComplete { r: Boolean?, throwable: Throwable? ->
if (throwable == null) {
log.i { "ApplicationService initialized" }
applicationBootstrapFacade.deactivate()
offerbookServiceFacade.activate()
marketPriceServiceFacade.activate()
} else {
log.e("Initializing applicationService failed", throwable)
}
}
}
} else {
offerbookServiceFacade.activate()
marketPriceServiceFacade.activate()
} else {
offerbookServiceFacade.activate()
marketPriceServiceFacade.activate()
}
}.onFailure { e ->
// TODO give user feedback (we could have a general error screen covering usual
// issues like connection issues and potential solutions)
log.e("Error at onViewAttached", e)
}
}

Expand Down
23 changes: 15 additions & 8 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
#Gradle
# Gradle
org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx8g"
org.gradle.caching=true
org.gradle.configuration-cache=true

#Kotlin
# Kotlin
kotlin.code.style=official

#Android
# Android
android.useAndroidX=true
android.nonTransitiveRClass=true

#Versioning
shared.version=0.0.9
# Versioning
shared.version=0.0.11

node.name=Bisq
node.android.version=0.0.5
node.android.version=0.0.7

client.name=BisqClient
client.android.version=0.0.3
client.ios.version=0.0.3
client.android.version=0.0.5
client.ios.version=0.0.5

# Networking

## Defaults for connectivity when not set by user
client.x.trustednode.port=8090
client.android.trustednode.ip=10.0.2.2
client.ios.trustednode.ip=localhost
29 changes: 18 additions & 11 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ androidx-test-espresso= "3.6.1"
androidx-lifecycle = "2.8.2"
androidx-test-compose-ver = "1.6.8"
androidx-multidex = "2.0.1"
atomicfu = "0.26.1"
bignum-lib= "0.3.10"
bisq-core = "2.1.2"
coilCompose = "3.0.3"
compose-plugin = "1.7.0"
junit = "4.13.2"
kotlinReflect = "2.0.21"
kotlinxDatetime = "0.4.0"
kotlinxSerializationCore = "1.7.3"
ktorClientCio = "3.0.1"
kotlinxSerialization = "1.7.3"
ktorClient = "3.0.1"
mockio = "1.12.0"
kotlin = "2.0.20"
kotlinTestJunit = "2.0.20"
Expand All @@ -37,7 +39,7 @@ kermit = "2.0.4"
buildconfig = "5.5.0"
navigationCompose = "2.7.0-alpha07"
okio = "3.9.1"
orgJetbrainsKotlinPluginSerializationGradlePlugin = "2.0.21"
serializationPlugin = "2.0.21"
protobuf = "0.9.4"
protob = "4.28.2"
ksp = "2.0.20-1.0.25"
Expand Down Expand Up @@ -73,19 +75,24 @@ typesafe-config-lib = { strictly = '1.4.3' }
multiplatform-settings = "1.2.0"

[libraries]
atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" }
jetbrains-kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlinReflect" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-test-junit-v180 = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlinTestJunit" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientCio" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktorClientCio" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientCio" }
ktor-client-json = { module = "io.ktor:ktor-client-json", version.ref = "ktorClientCio" }
ktor-client-serialization = { module = "io.ktor:ktor-client-serialization", version.ref = "ktorClientCio" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktorClientCio" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerialization" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClient" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktorClient" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClient" }
ktor-client-json = { module = "io.ktor:ktor-client-json", version.ref = "ktorClient" }
ktor-client-serialization = { module = "io.ktor:ktor-client-serialization", version.ref = "ktorClient" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktorClient" }
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktorClient" }

mock-io = { module = "io.mockk:mockk", version.ref = "mockio" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-multidex = { group = "androidx.multidex", name = "multidex", version.ref = "androidx-multidex" }
Expand Down Expand Up @@ -115,7 +122,7 @@ logging-kermit = { group = "co.touchlab", name = "kermit", version.ref = "kermit
lombok = { module = 'org.projectlombok:lombok', version.ref = 'lombok-lib' }

okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
jetbrains-serialization-gradle-plugin = { module = "org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin", version.ref = "orgJetbrainsKotlinPluginSerializationGradlePlugin" }
jetbrains-serialization-gradle-plugin = { module = "org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin", version.ref = "serializationPlugin" }
typesafe-config = { module = 'com.typesafe:config', version.ref = 'typesafe-config-lib' }

bouncycastle = { module = 'org.bouncycastle:bcprov-jdk18on', version.ref = 'bouncycastle-lib' }
Expand Down
8 changes: 4 additions & 4 deletions iosClient/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PODS:
- domain (0.0.9)
- presentation (0.0.9)
- domain (0.0.11)
- presentation (0.0.11)

DEPENDENCIES:
- domain (from `../shared/domain`)
Expand All @@ -13,8 +13,8 @@ EXTERNAL SOURCES:
:path: "../shared/presentation"

SPEC CHECKSUMS:
domain: feb764d7802f6a414d3f649e95721d16a38d018e
presentation: 8c21a3748676af08d28227a02cad92c9a7eeb27a
domain: 7b60b3f5ced94f5e4004f4239c7534bb7e8d0297
presentation: d9414a0a2c7e8cebf358a53de2c7043248139b68

PODFILE CHECKSUM: 431d52ef58584308462794999ebead56142b0160

Expand Down
2 changes: 1 addition & 1 deletion iosClient/Pods/Local Podspecs/domain.podspec.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion iosClient/Pods/Local Podspecs/presentation.podspec.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions iosClient/Pods/Manifest.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c643e26

Please sign in to comment.