Skip to content

Commit 374785d

Browse files
authored
(android) Add support for liquidity ads (#489)
This commit allows users to request inbound liquidity from their peer on the Android app. The iOS app will be updated later, in another commit. * Use lightning-kmp 1.5.13 * Add database and queries for inbound liquidity payments A liquidity lease rate is provided to the peer at startup using default values stored in the NodeParamsManager. Aggregated queries have been updated. Note that inbound liquidity payments use the `lockedAt` timestamp to define if the payment is complete or not. Also added a new `WalletPaymentId` for inbound liquidity payments, with internal code 6. * (android) Add UI for requesting liquidity A new screen has been added to request liquidity from the peer using liquidity-ads. The user can pick the amount, and get an estimation of the cost (mining + service fee) of the liquidity. Then accept the offer, or cancel the request by leaving the screen. The liquidity is done with a splice. A button to that screen has been added in the home and the liquidity policy screens. The channels view screen also shows the current inbound liquidity, using a linear progress bar.
1 parent 822bf2e commit 374785d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1387
-140
lines changed

buildSrc/src/main/kotlin/Versions.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
object Versions {
2-
const val lightningKmp = "1.5.12"
2+
const val lightningKmp = "1.5.13"
33
const val secp256k1 = "0.11.0"
44
const val torMobile = "0.2.0"
55

phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/AppView.kt

+9-3
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import fr.acinq.phoenix.android.settings.channels.ImportChannelsData
7878
import fr.acinq.phoenix.android.settings.displayseed.DisplaySeedView
7979
import fr.acinq.phoenix.android.settings.fees.AdvancedIncomingFeePolicy
8080
import fr.acinq.phoenix.android.settings.fees.LiquidityPolicyView
81+
import fr.acinq.phoenix.android.payments.liquidity.RequestLiquidityView
8182
import fr.acinq.phoenix.android.settings.walletinfo.FinalWalletInfo
8283
import fr.acinq.phoenix.android.settings.walletinfo.SwapInWalletInfo
8384
import fr.acinq.phoenix.android.settings.walletinfo.WalletInfoView
@@ -219,7 +220,8 @@ fun AppView(
219220
onTorClick = { navController.navigate(Screen.TorConfig) },
220221
onElectrumClick = { navController.navigate(Screen.ElectrumServer) },
221222
onShowSwapInWallet = { navController.navigate(Screen.WalletInfo.SwapInWallet) },
222-
onShowNotifications = { navController.navigate(Screen.Notifications) }
223+
onShowNotifications = { navController.navigate(Screen.Notifications) },
224+
onRequestLiquidityClick = { navController.navigate(Screen.LiquidityRequest.route) },
223225
)
224226
}
225227
}
@@ -322,7 +324,7 @@ fun AppView(
322324
}
323325
},
324326
onChannelClick = { navController.navigate("${Screen.ChannelDetails.route}?id=$it") },
325-
onImportChannelsDataClick = { navController.navigate(Screen.ImportChannelsData)}
327+
onImportChannelsDataClick = { navController.navigate(Screen.ImportChannelsData)},
326328
)
327329
}
328330
composable(
@@ -394,9 +396,13 @@ fun AppView(
394396
composable(Screen.LiquidityPolicy.route, deepLinks = listOf(navDeepLink { uriPattern ="phoenix:liquiditypolicy" })) {
395397
LiquidityPolicyView(
396398
onBackClick = { navController.popBackStack() },
397-
onAdvancedClick = { navController.navigate(Screen.AdvancedLiquidityPolicy.route) }
399+
onAdvancedClick = { navController.navigate(Screen.AdvancedLiquidityPolicy.route) },
400+
onRequestLiquidityClick = { navController.navigate(Screen.LiquidityRequest.route) },
398401
)
399402
}
403+
composable(Screen.LiquidityRequest.route, deepLinks = listOf(navDeepLink { uriPattern ="phoenix:requestliquidity" })) {
404+
RequestLiquidityView(onBackClick = { navController.popBackStack() },)
405+
}
400406
composable(Screen.AdvancedLiquidityPolicy.route) {
401407
AdvancedIncomingFeePolicy(onBackClick = { navController.popBackStack() })
402408
}

phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/Navigation.kt

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ sealed class Screen(val route: String) {
6060
object FinalWallet: Screen("settings/walletinfo/final")
6161
}
6262
object LiquidityPolicy: Screen("settings/liquiditypolicy")
63+
object LiquidityRequest: Screen("settings/requestliquidity")
6364
object AdvancedLiquidityPolicy: Screen("settings/advancedliquiditypolicy")
6465
object Notifications: Screen("notifications")
6566
object ResetWallet: Screen("settings/resetwallet")

phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/AmountView.kt

+26-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import androidx.compose.ui.semantics.Role
3434
import androidx.compose.ui.text.TextStyle
3535
import androidx.compose.ui.unit.Dp
3636
import androidx.compose.ui.unit.dp
37+
import androidx.compose.ui.unit.sp
3738
import fr.acinq.lightning.MilliSatoshi
3839
import fr.acinq.phoenix.android.*
3940
import fr.acinq.phoenix.android.R
@@ -148,7 +149,7 @@ fun AmountWithAltView(
148149
fun ColumnScope.AmountWithFiatBelow(
149150
amount: MilliSatoshi,
150151
amountTextStyle: TextStyle = MaterialTheme.typography.body1,
151-
fiatTextStyle: TextStyle = MaterialTheme.typography.caption,
152+
fiatTextStyle: TextStyle = MaterialTheme.typography.caption.copy(fontSize = 14.sp),
152153
) {
153154
val prefBtcUnit = LocalBitcoinUnit.current
154155
val prefFiatCurrency = LocalFiatCurrency.current
@@ -162,6 +163,30 @@ fun ColumnScope.AmountWithFiatBelow(
162163
)
163164
}
164165

166+
/** Outputs a column with the amount in bitcoin on top, and the fiat amount below. */
167+
@Composable
168+
fun AmountWithFiatBeside(
169+
amount: MilliSatoshi,
170+
amountTextStyle: TextStyle = MaterialTheme.typography.body1,
171+
fiatTextStyle: TextStyle = MaterialTheme.typography.caption.copy(fontSize = 14.sp),
172+
) {
173+
val prefBtcUnit = LocalBitcoinUnit.current
174+
val prefFiatCurrency = LocalFiatCurrency.current
175+
Row {
176+
Text(
177+
text = amount.toPrettyString(prefBtcUnit, withUnit = true),
178+
style = amountTextStyle,
179+
modifier = Modifier.alignByBaseline(),
180+
)
181+
Spacer(modifier = Modifier.width(6.dp))
182+
Text(
183+
text = stringResource(id = R.string.utils_converted_amount, amount.toPrettyString(prefFiatCurrency, fiatRate, withUnit = true)),
184+
style = fiatTextStyle,
185+
modifier = Modifier.alignByBaseline(),
186+
)
187+
}
188+
}
189+
165190
/** Outputs a row with the amount in bitcoin on the left, and the fiat amount on the right. */
166191
@Composable
167192
fun AmountWithFiatRowView(

phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/Buttons.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ fun BorderButton(
6060
text: String? = null,
6161
icon: Int? = null,
6262
iconTint: Color = MaterialTheme.colors.primary,
63+
shape: Shape = CircleShape,
6364
backgroundColor: Color = MaterialTheme.colors.surface,
6465
borderColor: Color = MaterialTheme.colors.primary,
6566
enabled: Boolean = true,
@@ -78,7 +79,7 @@ fun BorderButton(
7879
enabledEffect = enabledEffect,
7980
space = space,
8081
onClick = onClick,
81-
shape = CircleShape,
82+
shape = shape,
8283
backgroundColor = backgroundColor,
8384
border = BorderStroke(ButtonDefaults.OutlinedBorderSize, if (enabled) borderColor else borderColor.copy(alpha = 0.4f)),
8485
textStyle = textStyle,

phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/FeerateSlider.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ fun FeerateSlider(
9090
}
9191
}
9292

93-
SatoshiSlider(
93+
SatoshiLogSlider(
9494
modifier = Modifier
9595
.widthIn(max = 130.dp)
9696
.offset(x = (-4).dp, y = (-8).dp),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2023 ACINQ SAS
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package fr.acinq.phoenix.android.components
18+
19+
import androidx.compose.foundation.layout.Column
20+
import androidx.compose.foundation.layout.PaddingValues
21+
import androidx.compose.foundation.layout.Spacer
22+
import androidx.compose.foundation.layout.height
23+
import androidx.compose.material.MaterialTheme
24+
import androidx.compose.material.Slider
25+
import androidx.compose.material.SliderDefaults
26+
import androidx.compose.runtime.*
27+
import androidx.compose.ui.Modifier
28+
import androidx.compose.ui.graphics.Color
29+
import androidx.compose.ui.platform.LocalContext
30+
import androidx.compose.ui.unit.dp
31+
import fr.acinq.bitcoin.Satoshi
32+
import fr.acinq.lightning.utils.sat
33+
import fr.acinq.phoenix.android.R
34+
import fr.acinq.phoenix.android.components.feedback.ErrorMessage
35+
import kotlin.math.log10
36+
import kotlin.math.pow
37+
38+
/** A logarithmic slider to get a Satoshi value. Can be used to get a feerate for example. */
39+
@Composable
40+
fun SatoshiLogSlider(
41+
modifier: Modifier = Modifier,
42+
amount: Satoshi,
43+
onAmountChange: (Satoshi) -> Unit,
44+
minAmount: Satoshi = 1.sat,
45+
maxAmount: Satoshi = 500.sat,
46+
enabled: Boolean = true,
47+
steps: Int = 30,
48+
) {
49+
val context = LocalContext.current
50+
val minAmountLog = remember { log10(minAmount.sat.toFloat()) }
51+
val maxAmountLog = remember { log10(maxAmount.sat.toFloat()) }
52+
var amountLog by remember { mutableStateOf(log10(amount.sat.toFloat())) }
53+
54+
var errorMessage by remember { mutableStateOf("") }
55+
56+
Column(modifier = modifier.enableOrFade(enabled)) {
57+
Slider(
58+
value = amountLog,
59+
onValueChange = {
60+
errorMessage = ""
61+
try {
62+
amountLog = it
63+
val valueSat = 10f.pow(it).toLong().sat
64+
onAmountChange(valueSat)
65+
} catch (e: Exception) {
66+
errorMessage = context.getString(R.string.validation_invalid_number)
67+
}
68+
},
69+
valueRange = minAmountLog..maxAmountLog,
70+
steps = steps,
71+
enabled = enabled,
72+
colors = SliderDefaults.colors(
73+
activeTrackColor = MaterialTheme.colors.primary,
74+
inactiveTrackColor = MaterialTheme.colors.primary.copy(alpha = 0.4f),
75+
activeTickColor = MaterialTheme.colors.primary,
76+
inactiveTickColor = Color.Transparent,
77+
)
78+
)
79+
80+
errorMessage.takeUnless { it.isBlank() }?.let {
81+
Spacer(Modifier.height(4.dp))
82+
ErrorMessage(header = it, padding = PaddingValues(0.dp))
83+
}
84+
}
85+
}
86+

phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/SatoshiSlider.kt

+20-21
Original file line numberDiff line numberDiff line change
@@ -23,51 +23,49 @@ import androidx.compose.foundation.layout.height
2323
import androidx.compose.material.MaterialTheme
2424
import androidx.compose.material.Slider
2525
import androidx.compose.material.SliderDefaults
26-
import androidx.compose.runtime.*
26+
import androidx.compose.runtime.Composable
27+
import androidx.compose.runtime.getValue
28+
import androidx.compose.runtime.mutableStateOf
29+
import androidx.compose.runtime.remember
30+
import androidx.compose.runtime.setValue
2731
import androidx.compose.ui.Modifier
2832
import androidx.compose.ui.graphics.Color
2933
import androidx.compose.ui.platform.LocalContext
3034
import androidx.compose.ui.unit.dp
3135
import fr.acinq.bitcoin.Satoshi
32-
import fr.acinq.lightning.utils.sat
3336
import fr.acinq.phoenix.android.R
3437
import fr.acinq.phoenix.android.components.feedback.ErrorMessage
35-
import kotlin.math.log10
36-
import kotlin.math.pow
3738

38-
/** A logarithmic slider to get a Satoshi value. Can be used to get a feerate for example. */
39+
/** A slider to pick a Satoshi value from an array of accepted values. */
3940
@Composable
4041
fun SatoshiSlider(
4142
modifier: Modifier = Modifier,
42-
amount: Satoshi,
4343
onAmountChange: (Satoshi) -> Unit,
44-
minAmount: Satoshi = 1.sat,
45-
maxAmount: Satoshi = 500.sat,
44+
onErrorStateChange: (Boolean) -> Unit,
45+
possibleValues: Array<Satoshi>,
4646
enabled: Boolean = true,
47-
steps: Int = 30,
4847
) {
4948
val context = LocalContext.current
50-
val minFeerateLog = remember { log10(minAmount.sat.toFloat()) }
51-
val maxFeerateLog = remember { log10(maxAmount.sat.toFloat()) }
52-
var feerateLog by remember { mutableStateOf(log10(amount.sat.toFloat())) }
53-
49+
var index by remember { mutableStateOf(1.0f) }
5450
var errorMessage by remember { mutableStateOf("") }
5551

5652
Column(modifier = modifier.enableOrFade(enabled)) {
5753
Slider(
58-
value = feerateLog,
54+
value = index,
5955
onValueChange = {
6056
errorMessage = ""
6157
try {
62-
feerateLog = it
63-
val valueSat = 10f.pow(it).toLong().sat
64-
onAmountChange(valueSat)
58+
index = it
59+
val amountPicked = possibleValues[index.toInt() - 1]
60+
onAmountChange(amountPicked)
61+
onErrorStateChange(false)
6562
} catch (e: Exception) {
66-
errorMessage = context.getString(R.string.validation_invalid_number)
63+
errorMessage = context.getString(R.string.validation_invalid_amount)
64+
onErrorStateChange(true)
6765
}
6866
},
69-
valueRange = minFeerateLog..maxFeerateLog,
70-
steps = steps,
67+
valueRange = 1.0f..possibleValues.size.toFloat(),
68+
steps = possibleValues.size,
7169
enabled = enabled,
7270
colors = SliderDefaults.colors(
7371
activeTrackColor = MaterialTheme.colors.primary,
@@ -82,4 +80,5 @@ fun SatoshiSlider(
8280
ErrorMessage(header = it, padding = PaddingValues(0.dp))
8381
}
8482
}
85-
}
83+
}
84+

phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/Settings.kt

+16-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,19 @@ fun Setting(modifier: Modifier = Modifier, title: String, description: String?)
4747
}
4848
}
4949

50+
@Composable
51+
fun Setting(modifier: Modifier = Modifier, title: String, content: @Composable ColumnScope.() -> Unit) {
52+
Column(
53+
modifier
54+
.fillMaxWidth()
55+
.padding(horizontal = 16.dp, vertical = 12.dp)
56+
) {
57+
Text(title, style = MaterialTheme.typography.body2)
58+
Spacer(modifier = Modifier.height(2.dp))
59+
content()
60+
}
61+
}
62+
5063
@Composable
5164
fun SettingWithCopy(
5265
title: String,
@@ -56,7 +69,9 @@ fun SettingWithCopy(
5669
) {
5770
val context = LocalContext.current
5871
Row {
59-
Column(modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp).weight(1f)) {
72+
Column(modifier = Modifier
73+
.padding(start = 16.dp, top = 12.dp, bottom = 12.dp)
74+
.weight(1f)) {
6075
Row {
6176
Text(
6277
text = title,

phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/SplashLayout.kt

+4-3
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,16 @@ fun SplashLabelRow(
129129
.alignByBaseline(),
130130
) {
131131
Spacer(modifier = Modifier.weight(1f))
132-
if (helpMessage != null) {
133-
IconPopup(modifier = Modifier.offset(y = (-3).dp), popupMessage = helpMessage, spaceLeft = 0.dp, spaceRight = 4.dp)
134-
}
132+
135133
Text(
136134
text = label.uppercase(),
137135
style = MaterialTheme.typography.subtitle1.copy(fontSize = 12.sp, textAlign = TextAlign.End),
138136
maxLines = 2,
139137
overflow = TextOverflow.Ellipsis
140138
)
139+
if (helpMessage != null) {
140+
IconPopup(modifier = Modifier.offset(y = (-3).dp), popupMessage = helpMessage, spaceLeft = 4.dp, spaceRight = 0.dp)
141+
}
141142
if (icon != null) {
142143
Spacer(modifier = Modifier.width(4.dp))
143144
Image(

0 commit comments

Comments
 (0)