Skip to content

Commit cfd523f

Browse files
JOHNJOHN
authored andcommitted
Add Paykit integration module for Android (Phase 3.1)
Implements the core Paykit integration components: - PaykitManager: Singleton managing PaykitClient lifecycle and executor registration. Maps LDK Network to Paykit network configs. Uses Mutex for thread-safe initialization. - BitkitBitcoinExecutor: Implements BitcoinExecutorFFI, bridging LightningRepo.sendOnChain() to Paykit's onchain payment interface. Uses runBlocking with Dispatchers.IO for coroutine-to-sync bridging. - BitkitLightningExecutor: Implements LightningExecutorFFI, bridging LightningRepo.payInvoice() to Paykit. Includes payment completion polling to extract preimage proof, SHA-256 verification. - PaykitIntegrationHelper: Convenience methods for setup and payment execution with async callback support. Note: PaykitMobile bindings (UniFFI generated) must be linked before these components are fully functional. TODOs mark where binding code should be uncommented once available.
1 parent f42599e commit cfd523f

File tree

4 files changed

+815
-0
lines changed

4 files changed

+815
-0
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package to.bitkit.paykit
2+
3+
import kotlinx.coroutines.CoroutineScope
4+
import kotlinx.coroutines.Dispatchers
5+
import kotlinx.coroutines.SupervisorJob
6+
import kotlinx.coroutines.launch
7+
import to.bitkit.paykit.executors.BitkitBitcoinExecutor
8+
import to.bitkit.paykit.executors.BitkitLightningExecutor
9+
import to.bitkit.paykit.executors.BitcoinTxResult
10+
import to.bitkit.paykit.executors.LightningPaymentResult
11+
import to.bitkit.repositories.LightningRepo
12+
import to.bitkit.utils.Logger
13+
14+
/**
15+
* Helper object for setting up and managing Paykit integration.
16+
*
17+
* Provides convenience methods for common integration tasks.
18+
*/
19+
object PaykitIntegrationHelper {
20+
21+
private const val TAG = "PaykitIntegrationHelper"
22+
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
23+
24+
/**
25+
* Set up Paykit with Bitkit's Lightning repository.
26+
*
27+
* Call this during app startup after the wallet is ready.
28+
*
29+
* @param lightningRepo The LightningRepo instance for payment operations
30+
* @throws PaykitException if setup fails
31+
*/
32+
suspend fun setup(lightningRepo: LightningRepo) {
33+
val manager = PaykitManager.getInstance()
34+
35+
manager.initialize()
36+
manager.registerExecutors(lightningRepo)
37+
38+
Logger.info("Paykit integration setup complete", context = TAG)
39+
}
40+
41+
/**
42+
* Set up Paykit asynchronously.
43+
*
44+
* @param lightningRepo The LightningRepo instance
45+
* @param onComplete Callback with success/failure result
46+
*/
47+
fun setupAsync(
48+
lightningRepo: LightningRepo,
49+
onComplete: (Result<Unit>) -> Unit = {},
50+
) {
51+
scope.launch {
52+
try {
53+
setup(lightningRepo)
54+
onComplete(Result.success(Unit))
55+
} catch (e: Exception) {
56+
Logger.error("Paykit setup failed", e, context = TAG)
57+
onComplete(Result.failure(e))
58+
}
59+
}
60+
}
61+
62+
/**
63+
* Check if Paykit is ready for use.
64+
*/
65+
val isReady: Boolean
66+
get() {
67+
val manager = PaykitManager.getInstance()
68+
return manager.isInitialized && manager.hasExecutors
69+
}
70+
71+
/**
72+
* Get the current network configuration.
73+
*/
74+
val networkInfo: Pair<BitcoinNetworkConfig, LightningNetworkConfig>
75+
get() {
76+
val manager = PaykitManager.getInstance()
77+
return manager.bitcoinNetwork to manager.lightningNetwork
78+
}
79+
80+
/**
81+
* Execute a Lightning payment via Paykit.
82+
*
83+
* @param lightningRepo The LightningRepo instance
84+
* @param invoice BOLT11 invoice
85+
* @param amountSats Amount in satoshis (for zero-amount invoices)
86+
* @return Payment result
87+
* @throws PaykitException on failure
88+
*/
89+
suspend fun payLightning(
90+
lightningRepo: LightningRepo,
91+
invoice: String,
92+
amountSats: ULong?,
93+
): LightningPaymentResult {
94+
if (!isReady) {
95+
throw PaykitException.NotInitialized
96+
}
97+
98+
val executor = BitkitLightningExecutor(lightningRepo)
99+
val amountMsat = amountSats?.let { it * 1000uL }
100+
101+
return executor.payInvoice(
102+
invoice = invoice,
103+
amountMsat = amountMsat,
104+
maxFeeMsat = null,
105+
)
106+
}
107+
108+
/**
109+
* Execute an onchain payment via Paykit.
110+
*
111+
* @param lightningRepo The LightningRepo instance
112+
* @param address Bitcoin address
113+
* @param amountSats Amount in satoshis
114+
* @param feeRate Fee rate in sat/vB
115+
* @return Transaction result
116+
* @throws PaykitException on failure
117+
*/
118+
suspend fun payOnchain(
119+
lightningRepo: LightningRepo,
120+
address: String,
121+
amountSats: ULong,
122+
feeRate: Double?,
123+
): BitcoinTxResult {
124+
if (!isReady) {
125+
throw PaykitException.NotInitialized
126+
}
127+
128+
val executor = BitkitBitcoinExecutor(lightningRepo)
129+
130+
return executor.sendToAddress(
131+
address = address,
132+
amountSats = amountSats,
133+
feeRate = feeRate,
134+
)
135+
}
136+
137+
/**
138+
* Reset Paykit integration state.
139+
*
140+
* Call this during logout or wallet reset.
141+
*/
142+
fun reset() {
143+
PaykitManager.getInstance().reset()
144+
Logger.info("Paykit integration reset", context = TAG)
145+
}
146+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package to.bitkit.paykit
2+
3+
import kotlinx.coroutines.sync.Mutex
4+
import kotlinx.coroutines.sync.withLock
5+
import org.lightningdevkit.ldknode.Network
6+
import to.bitkit.env.Env
7+
import to.bitkit.paykit.executors.BitkitBitcoinExecutor
8+
import to.bitkit.paykit.executors.BitkitLightningExecutor
9+
import to.bitkit.repositories.LightningRepo
10+
import to.bitkit.utils.Logger
11+
import javax.inject.Inject
12+
import javax.inject.Singleton
13+
14+
/**
15+
* Manages PaykitClient lifecycle and executor registration for Bitkit Android.
16+
*
17+
* Usage:
18+
* ```kotlin
19+
* val manager = PaykitManager.getInstance()
20+
* manager.initialize()
21+
* manager.registerExecutors(lightningRepo)
22+
* ```
23+
*
24+
* Note: PaykitMobile bindings must be generated and linked before this class
25+
* is fully functional. See INTEGRATION_DISCOVERY.md for setup instructions.
26+
*/
27+
@Singleton
28+
class PaykitManager @Inject constructor() {
29+
30+
companion object {
31+
private const val TAG = "PaykitManager"
32+
33+
@Volatile
34+
private var instance: PaykitManager? = null
35+
36+
fun getInstance(): PaykitManager {
37+
return instance ?: synchronized(this) {
38+
instance ?: PaykitManager().also { instance = it }
39+
}
40+
}
41+
}
42+
43+
// The underlying Paykit client (null until initialized)
44+
// Type: PaykitClient from PaykitMobile bindings
45+
private var client: Any? = null
46+
47+
// Bitcoin executor instance (stored to prevent garbage collection)
48+
private var bitcoinExecutor: BitkitBitcoinExecutor? = null
49+
50+
// Lightning executor instance (stored to prevent garbage collection)
51+
private var lightningExecutor: BitkitLightningExecutor? = null
52+
53+
// Mutex for thread-safe initialization
54+
private val mutex = Mutex()
55+
56+
// Whether the manager has been initialized
57+
var isInitialized: Boolean = false
58+
private set
59+
60+
// Whether executors have been registered
61+
var hasExecutors: Boolean = false
62+
private set
63+
64+
// Bitcoin network configuration
65+
val bitcoinNetwork: BitcoinNetworkConfig = mapNetwork(Env.network).first
66+
67+
// Lightning network configuration
68+
val lightningNetwork: LightningNetworkConfig = mapNetwork(Env.network).second
69+
70+
/**
71+
* Maps LDK Network to Paykit network configs.
72+
*/
73+
private fun mapNetwork(network: Network): Pair<BitcoinNetworkConfig, LightningNetworkConfig> {
74+
return when (network) {
75+
Network.BITCOIN -> BitcoinNetworkConfig.MAINNET to LightningNetworkConfig.MAINNET
76+
Network.TESTNET -> BitcoinNetworkConfig.TESTNET to LightningNetworkConfig.TESTNET
77+
Network.REGTEST -> BitcoinNetworkConfig.REGTEST to LightningNetworkConfig.REGTEST
78+
Network.SIGNET -> BitcoinNetworkConfig.TESTNET to LightningNetworkConfig.TESTNET
79+
}
80+
}
81+
82+
/**
83+
* Initialize the Paykit client with network configuration.
84+
*
85+
* Call this during app startup, after the wallet is ready.
86+
*
87+
* @throws PaykitException if initialization fails
88+
*/
89+
suspend fun initialize() = mutex.withLock {
90+
if (isInitialized) {
91+
Logger.debug("PaykitManager already initialized", context = TAG)
92+
return@withLock
93+
}
94+
95+
Logger.info("Initializing PaykitManager with network: $bitcoinNetwork", context = TAG)
96+
97+
// TODO: Uncomment when PaykitMobile bindings are available
98+
// client = PaykitClient.newWithNetwork(
99+
// bitcoinNetwork = bitcoinNetwork.toFfi(),
100+
// lightningNetwork = lightningNetwork.toFfi()
101+
// )
102+
103+
isInitialized = true
104+
Logger.info("PaykitManager initialized successfully", context = TAG)
105+
}
106+
107+
/**
108+
* Register Bitcoin and Lightning executors with the Paykit client.
109+
*
110+
* This connects Bitkit's LightningRepo to Paykit for payment execution.
111+
* Must be called after [initialize].
112+
*
113+
* @param lightningRepo The LightningRepo instance for payment operations
114+
* @throws PaykitException if registration fails or client not initialized
115+
*/
116+
suspend fun registerExecutors(lightningRepo: LightningRepo) = mutex.withLock {
117+
if (!isInitialized) {
118+
throw PaykitException.NotInitialized
119+
}
120+
121+
if (hasExecutors) {
122+
Logger.debug("Executors already registered", context = TAG)
123+
return@withLock
124+
}
125+
126+
Logger.info("Registering Paykit executors", context = TAG)
127+
128+
// Create executor instances
129+
bitcoinExecutor = BitkitBitcoinExecutor(lightningRepo)
130+
lightningExecutor = BitkitLightningExecutor(lightningRepo)
131+
132+
// TODO: Uncomment when PaykitMobile bindings are available
133+
// val paykitClient = client as? PaykitClient
134+
// ?: throw PaykitException.NotInitialized
135+
//
136+
// paykitClient.registerBitcoinExecutor(bitcoinExecutor!!)
137+
// paykitClient.registerLightningExecutor(lightningExecutor!!)
138+
139+
hasExecutors = true
140+
Logger.info("Paykit executors registered successfully", context = TAG)
141+
}
142+
143+
/**
144+
* Reset the manager state (for testing or logout scenarios).
145+
*/
146+
fun reset() {
147+
client = null
148+
bitcoinExecutor = null
149+
lightningExecutor = null
150+
isInitialized = false
151+
hasExecutors = false
152+
Logger.info("PaykitManager reset", context = TAG)
153+
}
154+
}
155+
156+
/**
157+
* Bitcoin network configuration for Paykit.
158+
*/
159+
enum class BitcoinNetworkConfig {
160+
MAINNET,
161+
TESTNET,
162+
REGTEST;
163+
164+
// TODO: Uncomment when PaykitMobile bindings are available
165+
// fun toFfi(): BitcoinNetworkFfi = when (this) {
166+
// MAINNET -> BitcoinNetworkFfi.MAINNET
167+
// TESTNET -> BitcoinNetworkFfi.TESTNET
168+
// REGTEST -> BitcoinNetworkFfi.REGTEST
169+
// }
170+
}
171+
172+
/**
173+
* Lightning network configuration for Paykit.
174+
*/
175+
enum class LightningNetworkConfig {
176+
MAINNET,
177+
TESTNET,
178+
REGTEST;
179+
180+
// TODO: Uncomment when PaykitMobile bindings are available
181+
// fun toFfi(): LightningNetworkFfi = when (this) {
182+
// MAINNET -> LightningNetworkFfi.MAINNET
183+
// TESTNET -> LightningNetworkFfi.TESTNET
184+
// REGTEST -> LightningNetworkFfi.REGTEST
185+
// }
186+
}
187+
188+
/**
189+
* Errors that can occur during Paykit operations.
190+
*/
191+
sealed class PaykitException(message: String) : Exception(message) {
192+
object NotInitialized : PaykitException("PaykitManager has not been initialized")
193+
data class ExecutorRegistrationFailed(val reason: String) : PaykitException("Failed to register executor: $reason")
194+
data class PaymentFailed(val reason: String) : PaykitException("Payment failed: $reason")
195+
object Timeout : PaykitException("Operation timed out")
196+
data class Unknown(val reason: String) : PaykitException("Unknown error: $reason")
197+
}

0 commit comments

Comments
 (0)