Skip to content

Commit 5010919

Browse files
committed
Merge branch 'master' into mainnet
2 parents 0969dc1 + 92d14e9 commit 5010919

File tree

17 files changed

+181
-92
lines changed

17 files changed

+181
-92
lines changed

app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ android {
3333
applicationId "fr.acinq.phoenix.mainnet"
3434
minSdkVersion 24
3535
targetSdkVersion 30
36-
versionCode 27
37-
versionName "1.4.12"
36+
versionCode 28
37+
versionName "${gitCommitHash}"
3838
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
3939
}
4040
buildTypes {

app/src/main/java/fr/acinq/phoenix/AppContext.kt

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -203,49 +203,72 @@ class AppContext : Application(), DefaultLifecycleObserver {
203203
notifications.postValue(inAppNotifs)
204204

205205
// -- trampoline settings
206-
val trampolineArray = json.getJSONObject("trampoline").getJSONObject("v2").getJSONArray("attempts")
207-
val trampolineSettingsList = ArrayList<TrampolineFeeSetting>()
208-
for (i in 0 until trampolineArray.length()) {
209-
val setting: JSONObject = trampolineArray.get(i) as JSONObject
210-
trampolineSettingsList += TrampolineFeeSetting(
211-
feeBase = Satoshi(setting.getLong("fee_base_sat")),
212-
feeProportionalMillionths = setting.getLong("fee_per_millionths"),
213-
cltvExpiry = CltvExpiryDelta(setting.getInt("cltv_expiry")))
206+
val remoteTrampolineSettings = try {
207+
val trampolineArray = json.getJSONObject("trampoline").getJSONObject("v2").getJSONArray("attempts")
208+
val trampolineSettingsList = ArrayList<TrampolineFeeSetting>()
209+
for (i in 0 until trampolineArray.length()) {
210+
val setting: JSONObject = trampolineArray.get(i) as JSONObject
211+
trampolineSettingsList += TrampolineFeeSetting(
212+
feeBase = Satoshi(setting.getLong("fee_base_sat")),
213+
feeProportionalMillionths = setting.getLong("fee_per_millionths"),
214+
cltvExpiry = CltvExpiryDelta(setting.getInt("cltv_expiry")))
215+
}
216+
trampolineSettingsList.sortedWith(compareBy({ it.feeProportionalMillionths }, { it.feeBase }))
217+
trampolineSettingsList
218+
} catch (e: Exception) {
219+
log.warn("failed to read trampoline settings: ", e)
220+
Constants.DEFAULT_TRAMPOLINE_SETTINGS
214221
}
215-
trampolineSettingsList.sortedWith(compareBy({ it.feeProportionalMillionths }, { it.feeBase }))
216-
trampolineFeeSettings.postValue(trampolineSettingsList)
217-
log.info("trampoline settings=$trampolineSettingsList")
222+
trampolineFeeSettings.postValue(remoteTrampolineSettings)
223+
log.info("trampoline settings=$remoteTrampolineSettings")
218224

219225
// -- swap-out settings
220-
val remoteSwapOutSettings = json.getJSONObject("swap_out").getJSONObject("v1").run {
221-
SwapOutSettings(
222-
minFeerateSatByte = getLong("min_feerate_sat_byte").coerceAtLeast(0),
223-
status = ServiceStatus.valueOf(optInt("status"))
224-
)
226+
val remoteSwapOutSettings = try {
227+
json.getJSONObject("swap_out").getJSONObject("v1").run {
228+
SwapOutSettings(
229+
minFeerateSatByte = getLong("min_feerate_sat_byte").coerceAtLeast(0),
230+
minAmount = Satoshi(getLong("min_amount_sat")),
231+
maxAmount = Satoshi(getLong("max_amount_sat")),
232+
status = ServiceStatus.valueOf(optInt("status"))
233+
)
234+
}
235+
} catch (e: Exception) {
236+
log.warn("failed to read swap-out settings: ", e)
237+
Constants.DEFAULT_SWAP_OUT_SETTINGS
225238
}
226239
swapOutSettings.postValue(remoteSwapOutSettings)
227240
log.info("swap-out settings=$remoteSwapOutSettings")
228241

229242
// -- swap-in settings
230-
val remoteSwapInSettings = json.getJSONObject("swap_in").getJSONObject("v1").run {
231-
SwapInSettings(
232-
minFunding = Satoshi(getLong("min_funding_sat").coerceAtLeast(0)),
233-
minFee = Satoshi(getLong("min_fee_sat").coerceAtLeast(0)),
234-
feePercent = getDouble("fee_percent"),
235-
status = ServiceStatus.valueOf(optInt("status"))
236-
)
243+
val remoteSwapInSettings = try {
244+
json.getJSONObject("swap_in").getJSONObject("v1").run {
245+
SwapInSettings(
246+
minFunding = Satoshi(getLong("min_funding_sat").coerceAtLeast(0)),
247+
minFee = Satoshi(getLong("min_fee_sat").coerceAtLeast(0)),
248+
feePercent = getDouble("fee_percent"),
249+
status = ServiceStatus.valueOf(optInt("status"))
250+
)
251+
}
252+
} catch (e: Exception) {
253+
log.warn("failed to read swap-in settings: ", e)
254+
null
237255
}
238256
swapInSettings.postValue(remoteSwapInSettings)
239257
log.info("swap-in settings=$remoteSwapInSettings")
240258

241259
// -- pay-to-open settings
242-
val remotePayToOpenSettings = json.getJSONObject("pay_to_open").getJSONObject("v1").run {
243-
PayToOpenSettings(
244-
minFunding = Satoshi(getLong("min_funding_sat").coerceAtLeast(0)),
245-
minFee = Satoshi(getLong("min_fee_sat").coerceAtLeast(0)),
246-
feePercent = getDouble("fee_percent"),
247-
status = ServiceStatus.valueOf(optInt("status"))
248-
)
260+
val remotePayToOpenSettings = try {
261+
json.getJSONObject("pay_to_open").getJSONObject("v1").run {
262+
PayToOpenSettings(
263+
minFunding = Satoshi(getLong("min_funding_sat").coerceAtLeast(0)),
264+
minFee = Satoshi(getLong("min_fee_sat").coerceAtLeast(0)),
265+
feePercent = getDouble("fee_percent"),
266+
status = ServiceStatus.valueOf(optInt("status"))
267+
)
268+
}
269+
} catch (e: Exception) {
270+
log.warn("failed to read pay-to-open settings: ", e)
271+
null
249272
}
250273
payToOpenSettings.postValue(remotePayToOpenSettings)
251274
log.info("pay-to-open settings=$remotePayToOpenSettings")
@@ -352,7 +375,7 @@ data class TrampolineFeeSetting(val feeBase: Satoshi, val feeProportionalMillion
352375
}
353376

354377
data class SwapInSettings(val minFunding: Satoshi, val minFee: Satoshi, val feePercent: Double, val status: ServiceStatus)
355-
data class SwapOutSettings(val minFeerateSatByte: Long, val status: ServiceStatus)
378+
data class SwapOutSettings(val minFeerateSatByte: Long, val minAmount: Satoshi, val maxAmount: Satoshi, val status: ServiceStatus)
356379
data class MempoolContext(val highUsageWarning: Boolean)
357380
data class PayToOpenSettings(val minFunding: Satoshi, val minFee: Satoshi, val feePercent: Double, val status: ServiceStatus)
358381
data class Balance(val channelsCount: Int, val sendable: MilliSatoshi, val receivable: MilliSatoshi)

app/src/main/java/fr/acinq/phoenix/lnurl/LNUrlAuthFragment.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import android.view.View
2323
import android.view.ViewGroup
2424
import androidx.annotation.UiThread
2525
import androidx.lifecycle.MutableLiveData
26-
import androidx.lifecycle.Observer
2726
import androidx.lifecycle.ViewModel
2827
import androidx.lifecycle.ViewModelProvider
2928
import androidx.lifecycle.lifecycleScope
@@ -40,6 +39,7 @@ import fr.acinq.phoenix.background.KitState
4039
import fr.acinq.phoenix.databinding.FragmentLnurlAuthBinding
4140
import fr.acinq.phoenix.utils.Converter
4241
import fr.acinq.phoenix.utils.KitNotInitialized
42+
import fr.acinq.phoenix.utils.LangExtensions.findNavControllerSafe
4343
import kotlinx.coroutines.CoroutineExceptionHandler
4444
import kotlinx.coroutines.Dispatchers
4545
import kotlinx.coroutines.delay
@@ -73,7 +73,7 @@ class LNUrlAuthFragment : BaseFragment() {
7373
HttpUrl.parse(args.url.url)!!
7474
} catch (e: Exception) {
7575
log.error("could not parse url=${args.url.url}: ", e)
76-
findNavController().popBackStack()
76+
findNavControllerSafe()?.popBackStack()
7777
return
7878
}
7979
model = ViewModelProvider(this, LNUrlAuthViewModel.Factory(url)).get(LNUrlAuthViewModel::class.java)
@@ -90,7 +90,7 @@ class LNUrlAuthFragment : BaseFragment() {
9090
}
9191
is LNUrlAuthState.Done -> Handler().postDelayed({
9292
if (model.state.value is LNUrlAuthState.Done) {
93-
findNavController().popBackStack()
93+
findNavControllerSafe()?.popBackStack()
9494
}
9595
}, 3000)
9696
else -> Unit

app/src/main/java/fr/acinq/phoenix/send/SendFragment.kt

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class SendFragment : BaseFragment() {
6161
private lateinit var model: SendViewModel
6262

6363
private lateinit var unitList: List<String>
64+
private val swapOutSettings by lazy { appContext()?.swapOutSettings?.value ?: Constants.DEFAULT_SWAP_OUT_SETTINGS }
6465
private val minFeerateSwapout by lazy { getMinSwapoutFeerate() }
6566

6667
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@@ -136,11 +137,16 @@ class SendFragment : BaseFragment() {
136137
}
137138
})
138139

139-
model.amountErrorMessage.observe(viewLifecycleOwner, { msgId ->
140+
model.amountError.observe(viewLifecycleOwner, { error ->
140141
if (model.isAmountFieldPristine.value != true) {
141-
if (msgId != null) {
142+
if (error != null) {
142143
mBinding.amountConverted.text = ""
143-
mBinding.amountError.text = getString(msgId)
144+
mBinding.amountError.text = when (error) {
145+
AmountError.NotEnoughBalance -> getString(R.string.send_amount_error_balance)
146+
AmountError.SwapOutBelowMin -> getString(R.string.send_amount_error_swap_out_too_small, Converter.printAmountPretty(swapOutSettings.minAmount, requireContext(), withUnit = true))
147+
AmountError.SwapOutAboveMax -> getString(R.string.send_amount_error_swap_out_above_max, Converter.printAmountPretty(swapOutSettings.maxAmount, requireContext(), withUnit = true))
148+
else -> getString(R.string.send_amount_error)
149+
}
144150
mBinding.amountError.visibility = View.VISIBLE
145151
} else {
146152
mBinding.amountError.visibility = View.GONE
@@ -281,11 +287,10 @@ class SendFragment : BaseFragment() {
281287
}
282288

283289
private fun getMinSwapoutFeerate(): Long {
284-
val minSetting = appContext()?.swapOutSettings?.value?.minFeerateSatByte ?: 1
285-
return if (minSetting == 0L) {
290+
return if (swapOutSettings.minFeerateSatByte == 0L) {
286291
(app.state.value?.kit()?.nodeParams()?.onChainFeeConf()?.feeEstimator()?.getFeeratePerKb(36) ?: 1000) / 1000
287292
} else {
288-
minSetting
293+
swapOutSettings.minFeerateSatByte
289294
}
290295
}
291296

@@ -355,7 +360,7 @@ class SendFragment : BaseFragment() {
355360
val unit = mBinding.unit.selectedItem.toString()
356361
val amountInput = mBinding.amount.text.toString()
357362
val balance = appContext(requireContext()).balance.value
358-
model.amountErrorMessage.value = null
363+
model.amountError.value = null
359364
val fiat = Prefs.getFiatCurrency(ctx)
360365
val amount = if (unit == fiat) {
361366
Option.apply(Converter.convertFiatToMsat(ctx, amountInput))
@@ -369,23 +374,21 @@ class SendFragment : BaseFragment() {
369374
mBinding.amountConverted.text = getString(R.string.utils_converted_amount, Converter.printFiatPretty(ctx, amount.get(), withUnit = true))
370375
}
371376
if (balance != null && amount.get().`$greater`(balance.sendable)) {
372-
throw InsufficientBalance()
377+
throw AmountError.NotEnoughBalance
373378
}
374-
if (model.state.value is SendState.Onchain && amount.get().`$less`(Satoshi(10000))) {
375-
throw SwapOutInsufficientAmount()
379+
if (model.state.value is SendState.Onchain && amount.get().`$less`(swapOutSettings.minAmount)) {
380+
throw AmountError.SwapOutBelowMin
381+
} else if (model.state.value is SendState.Onchain && amount.get().`$greater`(swapOutSettings.maxAmount)) {
382+
throw AmountError.SwapOutAboveMax
376383
}
377384
} else {
378385
throw RuntimeException("amount is undefined")
379386
}
380387
amount
381388
} catch (e: Exception) {
382-
log.debug("could not check amount: ${e.message}")
389+
log.debug("user entered an invalid amount: ${e.message ?: e::class.java.simpleName}")
383390
mBinding.amountConverted.text = ""
384-
model.amountErrorMessage.value = when (e) {
385-
is SwapOutInsufficientAmount -> R.string.send_amount_error_swap_out_too_small
386-
is InsufficientBalance -> R.string.send_amount_error_balance
387-
else -> R.string.send_amount_error
388-
}
391+
model.amountError.value = e
389392
Option.empty()
390393
}
391394
}

app/src/main/java/fr/acinq/phoenix/send/SendViewModel.kt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ import fr.acinq.bitcoin.Satoshi
2222
import fr.acinq.eclair.db.OutgoingPaymentStatus
2323
import fr.acinq.eclair.payment.PaymentRequest
2424
import fr.acinq.phoenix.background.EclairNodeService
25-
import fr.acinq.phoenix.utils.BitcoinURI
26-
import fr.acinq.phoenix.utils.Constants
27-
import fr.acinq.phoenix.utils.SingleLiveEvent
28-
import fr.acinq.phoenix.utils.Wallet
25+
import fr.acinq.phoenix.utils.*
2926
import kotlinx.coroutines.Dispatchers
3027
import kotlinx.coroutines.launch
3128
import org.slf4j.LoggerFactory
@@ -72,7 +69,7 @@ class SendViewModel : ViewModel() {
7269
val isAmountFieldPristine = MutableLiveData<Boolean>()
7370
val useMaxBalance = MutableLiveData<Boolean>()
7471
/** Contains strings resource id for amount error message. Not contained in the fragment Error state because an incorrect amount is not a fatal error. */
75-
val amountErrorMessage = SingleLiveEvent<Int>()
72+
val amountError = MutableLiveData<Exception?>()
7673
val showFeeratesForm = MutableLiveData<Boolean>()
7774
val chainFeesSatBytes = MutableLiveData<Long>()
7875

@@ -82,7 +79,7 @@ class SendViewModel : ViewModel() {
8279
state.value = SendState.CheckingInvoice
8380
useMaxBalance.value = false
8481
isAmountFieldPristine.value = true
85-
amountErrorMessage.value = null
82+
amountError.value = null
8683
showFeeratesForm.value = false // by default, show a lean view without advanced stuff
8784
chainFeesSatBytes.value = 3 // base fee in sat/bytes
8885
}

app/src/main/java/fr/acinq/phoenix/settings/adapters/ChannelHolder.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import fr.acinq.eclair.channel.HasCommitments
2727
import fr.acinq.eclair.channel.RES_GETINFO
2828
import fr.acinq.phoenix.R
2929
import fr.acinq.phoenix.settings.ListChannelsFragmentDirections
30+
import fr.acinq.phoenix.utils.LangExtensions.findNavControllerSafe
3031
import fr.acinq.phoenix.utils.Transcriber
3132
import fr.acinq.phoenix.utils.customviews.CoinView
3233
import org.slf4j.Logger
@@ -67,7 +68,7 @@ class ChannelHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
6768

6869
itemView.setOnClickListener {
6970
val action = ListChannelsFragmentDirections.actionListChannelsToChannelDetails(channel.channelId().toString())
70-
itemView.findNavController().navigate(action)
71+
itemView.findNavControllerSafe()?.navigate(action)
7172
}
7273
}
7374
}

app/src/main/java/fr/acinq/phoenix/utils/Constants.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ object Constants {
5858
// -- default wallet values
5959
val DEFAULT_FEERATE = FeerateEstimationPerKb(rate20min = 12, rate60min = 6, rate12hours = 3)
6060
val DEFAULT_NETWORK_INFO = NetworkInfo(electrumServer = null, lightningConnected = false, torConnections = HashMap())
61+
6162
// these default values will be overridden by fee settings from remote, with up-to-date values
6263
val DEFAULT_TRAMPOLINE_SETTINGS = listOf(
6364
TrampolineFeeSetting(Satoshi(0), 0, CltvExpiryDelta(576)), // 0 sat + 0.0 %
@@ -67,7 +68,11 @@ object Constants {
6768
TrampolineFeeSetting(Satoshi(7), 1000, CltvExpiryDelta(576)), // 7 sat + 0.1 %
6869
TrampolineFeeSetting(Satoshi(10), 1200, CltvExpiryDelta(576)), // 10 sat + 0.12 %
6970
TrampolineFeeSetting(Satoshi(12), 3000, CltvExpiryDelta(576))) // 12 sat + 0.3 %
70-
val DEFAULT_SWAP_OUT_SETTINGS = SwapOutSettings(10, ServiceStatus.Active)
71+
val DEFAULT_SWAP_OUT_SETTINGS = SwapOutSettings(
72+
minFeerateSatByte = 5,
73+
minAmount = Satoshi(10_000),
74+
maxAmount = Satoshi(2_000_000), // 0.02 BTC
75+
status = ServiceStatus.Active)
7176
val DEFAULT_MEMPOOL_CONTEXT = MempoolContext(false)
7277
val DEFAULT_BALANCE = Balance(0, MilliSatoshi(0), MilliSatoshi(0))
7378
}

app/src/main/java/fr/acinq/phoenix/utils/Exceptions.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ object ServiceDisconnected : RuntimeException("node service is disconnected")
3030
class ChannelsNotClosed(channelsNotClosedCount: Int) : RuntimeException()
3131

3232
// -- payment exceptions
33-
class InsufficientBalance : RuntimeException()
34-
class SwapOutInsufficientAmount : RuntimeException()
33+
sealed class AmountError: RuntimeException() {
34+
object Default : AmountError()
35+
object NotEnoughBalance : AmountError()
36+
object SwapOutBelowMin : AmountError()
37+
object SwapOutAboveMax : AmountError()
38+
}
3539
object CannotSendHeadless : RuntimeException("the service cannot send a payment while being headless")
3640

3741
// -- parsing exceptions

app/src/main/java/fr/acinq/phoenix/utils/LangExtensions.kt

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616

1717
package fr.acinq.phoenix.utils
1818

19-
import kotlin.contracts.ExperimentalContracts
20-
import kotlin.contracts.InvocationKind
21-
import kotlin.contracts.contract
19+
import android.view.View
20+
import androidx.fragment.app.Fragment
21+
import androidx.navigation.NavController
22+
import androidx.navigation.Navigation
23+
import androidx.navigation.fragment.NavHostFragment
24+
import org.slf4j.Logger
25+
import org.slf4j.LoggerFactory
2226

2327
/**
2428
* Utility method rebinding any exceptions thrown by a method into another exception, using the origin exception as the root cause.
@@ -30,3 +34,20 @@ inline fun <T> tryWith(exception: Exception, action: () -> T): T = try {
3034
exception.initCause(t)
3135
throw exception
3236
}
37+
object LangExtensions {
38+
val log: Logger = LoggerFactory.getLogger(this::class.java)
39+
40+
fun Fragment.findNavControllerSafe(): NavController? = try {
41+
NavHostFragment.findNavController(this)
42+
} catch (e: Exception) {
43+
log.warn("failed to find navigation controller in fragment=${this::class.qualifiedName.toString()}")
44+
null
45+
}
46+
47+
fun View.findNavControllerSafe(): NavController? = try {
48+
Navigation.findNavController(this)
49+
} catch (e: Exception) {
50+
log.warn("failed to find navigation controller in view=${this::class.qualifiedName.toString()}")
51+
null
52+
}
53+
}

app/src/main/res/values-cs/strings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
<string name="send_invalid_pr_generic">Tato platba není platná. Musel jste naskenovat neplatný požadavek.\n\nProsím, zkuste to znovu.</string>
116116
<string name="send_amount_error">Prosím, zadejte platné množství.</string>
117117
<string name="send_amount_error_balance">Množství je vyšší než váš zůstatek.</string>
118-
<string name="send_amount_error_swap_out_too_small">Minimálně 10000 satoshi za on-chain platbu.</string>
118+
<string name="send_amount_error_swap_out_too_small">Minimálně %1$s za on-chain platbu.</string>
119119
<string name="send_destination_label">Odeslat na</string>
120120
<string name="send_description_label">Popis</string>
121121
<string name="send_error_sending">Nastala chyba při odesílání platby. Žádné peníze nebyly odeslány.</string>

0 commit comments

Comments
 (0)