@@ -10,59 +10,105 @@ import com.synonym.vssclient.vssNewClientWithLnurlAuth
1010import com.synonym.vssclient.vssStore
1111import kotlinx.coroutines.CompletableDeferred
1212import kotlinx.coroutines.CoroutineDispatcher
13+ import kotlinx.coroutines.delay
14+ import kotlinx.coroutines.sync.Mutex
15+ import kotlinx.coroutines.sync.withLock
1316import kotlinx.coroutines.withContext
1417import kotlinx.coroutines.withTimeout
1518import to.bitkit.data.keychain.Keychain
1619import to.bitkit.di.IoDispatcher
1720import to.bitkit.env.Env
1821import to.bitkit.utils.Logger
19- import to.bitkit.utils.ServiceError
2022import javax.inject.Inject
2123import javax.inject.Singleton
2224import kotlin.time.Duration.Companion.seconds
2325
26+ class MnemonicNotAvailableException : Exception (" Mnemonic not available" )
27+
2428@Singleton
2529class VssBackupClient @Inject constructor(
2630 @IoDispatcher private val ioDispatcher : CoroutineDispatcher ,
2731 private val vssStoreIdProvider : VssStoreIdProvider ,
2832 private val keychain : Keychain ,
2933) {
3034 private var isSetup = CompletableDeferred <Unit >()
35+ private val setupMutex = Mutex ()
3136
32- suspend fun setup (walletIndex : Int = 0) = withContext(ioDispatcher) {
33- runCatching {
34- withTimeout(30 .seconds) {
35- Logger .debug(" VSS client setting up…" , context = TAG )
36- val vssUrl = Env .vssServerUrl
37- val lnurlAuthServerUrl = Env .lnurlAuthServerUrl
38- val vssStoreId = vssStoreIdProvider.getVssStoreId(walletIndex)
39- Logger .verbose(" Building VSS client with vssUrl: '$vssUrl '" , context = TAG )
40- Logger .verbose(" Building VSS client with lnurlAuthServerUrl: '$lnurlAuthServerUrl '" , context = TAG )
41- if (lnurlAuthServerUrl.isNotEmpty()) {
42- val mnemonic = keychain.loadString(Keychain .Key .BIP39_MNEMONIC .name)
43- ? : throw ServiceError .MnemonicNotFound ()
44- val passphrase = keychain.loadString(Keychain .Key .BIP39_PASSPHRASE .name)
37+ suspend fun setup (walletIndex : Int = 0): Result <Unit > = withContext(ioDispatcher) {
38+ setupMutex.withLock {
39+ runCatching {
40+ if (isSetup.isCompleted && ! isSetup.isCancelled) {
41+ runCatching { isSetup.await() }.onSuccess { return @runCatching }
42+ }
43+
44+ val mnemonic = keychain.loadString(Keychain .Key .BIP39_MNEMONIC .name)
45+ ? : throw MnemonicNotAvailableException ()
4546
46- vssNewClientWithLnurlAuth(
47- baseUrl = vssUrl,
48- storeId = vssStoreId,
49- mnemonic = mnemonic,
50- passphrase = passphrase,
51- lnurlAuthServerUrl = lnurlAuthServerUrl,
52- )
53- } else {
54- vssNewClient(
55- baseUrl = vssUrl,
56- storeId = vssStoreId,
57- )
47+ withTimeout(30 .seconds) {
48+ Logger .debug(" VSS client setting up…" , context = TAG )
49+ val vssUrl = Env .vssServerUrl
50+ val lnurlAuthServerUrl = Env .lnurlAuthServerUrl
51+ val vssStoreId = vssStoreIdProvider.getVssStoreId(walletIndex)
52+ Logger .verbose(" Building VSS client with vssUrl: '$vssUrl '" , context = TAG )
53+ Logger .verbose(" Building VSS client with lnurlAuthServerUrl: '$lnurlAuthServerUrl '" , context = TAG )
54+ if (lnurlAuthServerUrl.isNotEmpty()) {
55+ val passphrase = keychain.loadString(Keychain .Key .BIP39_PASSPHRASE .name)
56+
57+ vssNewClientWithLnurlAuth(
58+ baseUrl = vssUrl,
59+ storeId = vssStoreId,
60+ mnemonic = mnemonic,
61+ passphrase = passphrase,
62+ lnurlAuthServerUrl = lnurlAuthServerUrl,
63+ )
64+ } else {
65+ vssNewClient(
66+ baseUrl = vssUrl,
67+ storeId = vssStoreId,
68+ )
69+ }
70+ isSetup.complete(Unit )
71+ Logger .info(" VSS client setup with server: '$vssUrl '" , context = TAG )
5872 }
59- isSetup.complete(Unit )
60- Logger .info(" VSS client setup with server: '$vssUrl '" , context = TAG )
73+ }.onFailure {
74+ isSetup.completeExceptionally(it)
75+ Logger .error(" VSS client setup error" , it, context = TAG )
76+ }
77+ }
78+ }
79+
80+ class SetupRetryLogger {
81+ var onSuccess: (attempt: Int ) -> Unit = {}
82+ var onRetry: (attempt: Int , maxAttempts: Int , delayMs: Long ) -> Unit = { _, _, _ -> }
83+ var onExhausted: (maxAttempts: Int ) -> Unit = {}
84+ }
85+
86+ suspend fun setupWithRetry (
87+ maxAttempts : Int = 10,
88+ baseDelayMs : Long = 1000L,
89+ logger : SetupRetryLogger .() -> Unit ,
90+ ): Result <Unit > = withContext(ioDispatcher) {
91+ val log = SetupRetryLogger ().apply (logger)
92+ var attempt = 0
93+ while (attempt < maxAttempts) {
94+ val result = setup()
95+ if (result.isSuccess) {
96+ log.onSuccess(attempt + 1 )
97+ return @withContext Result .success(Unit )
98+ }
99+ val exception = result.exceptionOrNull()
100+ if (exception != null && exception !is MnemonicNotAvailableException ) {
101+ return @withContext result
102+ }
103+ attempt++
104+ if (attempt < maxAttempts) {
105+ val delayMs = baseDelayMs * attempt
106+ log.onRetry(attempt, maxAttempts, delayMs)
107+ delay(delayMs)
61108 }
62- }.onFailure {
63- isSetup.completeExceptionally(it)
64- Logger .error(" VSS client setup error" , e = it, context = TAG )
65109 }
110+ log.onExhausted(maxAttempts)
111+ Result .failure(MnemonicNotAvailableException ())
66112 }
67113
68114 fun reset () {
0 commit comments