Skip to content

Commit c684039

Browse files
authored
Merge pull request #194 from synonymdev/feat/ldk-node-fork
LDK-node fork update: tx boosting & utxo selection
2 parents e1e5711 + 3470bac commit c684039

File tree

14 files changed

+935
-85
lines changed

14 files changed

+935
-85
lines changed

app/build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,9 @@ dependencies {
168168
implementation(libs.camera.view)
169169
// Crypto
170170
implementation(libs.bouncycastle.provider.jdk)
171-
implementation(libs.ldk.node.android)
171+
implementation(libs.ldk.node.android) {
172+
exclude(group = "net.java.dev.jna", module = "jna") // fix for ldk-node fork builds
173+
}
172174
// Firebase
173175
implementation(platform(libs.firebase.bom))
174176
implementation(libs.firebase.messaging)
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
package to.bitkit.services
2+
3+
import android.content.Context
4+
import androidx.test.core.app.ApplicationProvider
5+
import androidx.test.ext.junit.runners.AndroidJUnit4
6+
import dagger.hilt.android.testing.HiltAndroidRule
7+
import dagger.hilt.android.testing.HiltAndroidTest
8+
import kotlinx.coroutines.delay
9+
import kotlinx.coroutines.runBlocking
10+
import org.junit.After
11+
import org.junit.Before
12+
import org.junit.Rule
13+
import org.junit.Test
14+
import org.junit.runner.RunWith
15+
import to.bitkit.data.keychain.Keychain
16+
import to.bitkit.env.Env
17+
import to.bitkit.repositories.WalletRepo
18+
import javax.inject.Inject
19+
import kotlin.test.assertEquals
20+
import kotlin.test.assertFalse
21+
import kotlin.test.assertNotNull
22+
import kotlin.test.assertTrue
23+
24+
@HiltAndroidTest
25+
@RunWith(AndroidJUnit4::class)
26+
class TxBumpingTests {
27+
28+
@get:Rule
29+
var hiltRule = HiltAndroidRule(this)
30+
31+
@Inject
32+
lateinit var lightningService: LightningService
33+
34+
@Inject
35+
lateinit var coreService: CoreService
36+
37+
@Inject
38+
lateinit var keychain: Keychain
39+
40+
@Inject
41+
lateinit var walletRepo: WalletRepo
42+
43+
private val walletIndex = 0
44+
45+
@Before
46+
fun setUp() {
47+
Env.initAppStoragePath(ApplicationProvider.getApplicationContext<Context>().filesDir.absolutePath)
48+
hiltRule.inject()
49+
println("Starting TX bumping test setup")
50+
51+
// Wipe the keychain before starting tests
52+
println("Wiping keychain before test")
53+
runBlocking {
54+
keychain.wipe()
55+
}
56+
println("Keychain wiped successfully")
57+
}
58+
59+
@After
60+
fun tearDown() {
61+
runBlocking {
62+
println("Tearing down TX bumping test")
63+
64+
if (lightningService.status?.isRunning == true) {
65+
try {
66+
lightningService.stop()
67+
} catch (e: Exception) {
68+
println("Error stopping lightning service: ${e.message}")
69+
}
70+
}
71+
try {
72+
lightningService.wipeStorage(walletIndex = walletIndex)
73+
} catch (e: Exception) {
74+
println("Error wiping lightning storage: ${e.message}")
75+
}
76+
77+
// Wipe the keychain after test completion
78+
println("Wiping keychain after test")
79+
keychain.wipe()
80+
println("Keychain wiped successfully")
81+
}
82+
}
83+
84+
@Test
85+
fun testBumpFeeByRbf() = runBlocking {
86+
println("Starting bump fee by RBF test")
87+
88+
// Create a new wallet using WalletRepo
89+
println("Creating new wallet")
90+
walletRepo.createWallet(bip39Passphrase = null)
91+
lightningService.setup(walletIndex = walletIndex)
92+
93+
println("Starting lightning node")
94+
lightningService.start()
95+
println("Lightning node started successfully")
96+
97+
// Test wallet sync
98+
println("Syncing wallet")
99+
lightningService.sync()
100+
println("Wallet sync complete")
101+
102+
// Generate an address to receive funds
103+
println("Generating deposit address")
104+
val depositAddress = lightningService.newAddress()
105+
println("Deposit address: $depositAddress")
106+
107+
// Fund the wallet with a single transaction
108+
val depositAmount = 100_000uL // 100,000 sats
109+
println("Depositing $depositAmount sats to wallet")
110+
coreService.blocktank.regtestMine(1u)
111+
val fundingTxId = coreService.blocktank.regtestDeposit(
112+
address = depositAddress,
113+
amountSat = depositAmount
114+
)
115+
assertFalse(fundingTxId.isEmpty(), "Funding transaction ID should not be empty")
116+
println("Funding transaction ID: $fundingTxId")
117+
118+
// Mine blocks to confirm the funding transaction
119+
println("Mining 6 blocks to confirm funding transaction")
120+
coreService.blocktank.regtestMine(6u)
121+
println("Blocks mined successfully")
122+
123+
// Wait for blocks to be processed
124+
println("Waiting 15 seconds for blocks to be processed")
125+
delay(15_000)
126+
println("Wait completed")
127+
128+
// Sync the wallet to see the new balance
129+
println("Syncing wallet to update balances")
130+
lightningService.sync()
131+
println("Wallet sync complete")
132+
133+
// Verify we have the expected balance
134+
val balances = lightningService.balances
135+
assertNotNull(balances, "Balances should not be null")
136+
val totalBalance = balances.totalOnchainBalanceSats
137+
println("Current balance: $totalBalance sats")
138+
assertEquals(depositAmount, totalBalance, "Balance should equal deposit amount")
139+
140+
// Send a transaction with a low fee rate
141+
val destinationAddress = "bcrt1qs04g2ka4pr9s3mv73nu32tvfy7r3cxd27wkyu8"
142+
val sendAmount = 10_000uL // Send 10,000 sats
143+
val lowFeeRate = 1u // 1 sat/vbyte (very low)
144+
145+
println("Sending $sendAmount sats to $destinationAddress with low fee rate of $lowFeeRate sat/vbyte")
146+
val originalTxId = lightningService.send(
147+
address = destinationAddress,
148+
sats = sendAmount,
149+
satsPerVByte = lowFeeRate,
150+
)
151+
152+
lightningService.sync()
153+
154+
assertFalse(originalTxId.isEmpty(), "Original transaction ID should not be empty")
155+
println("Original transaction sent with txid: $originalTxId")
156+
157+
// Wait a moment before attempting to bump the fee
158+
println("Waiting 2 seconds before bumping fee")
159+
delay(2_000)
160+
println("Wait completed")
161+
162+
// Bump the fee using RBF with a higher fee rate
163+
val highFeeRate = 10u // 10 sat/vbyte (much higher)
164+
println("Bumping fee for transaction $originalTxId to $highFeeRate sat/vbyte using RBF")
165+
166+
val replacementTxId = lightningService.bumpFeeByRbf(
167+
txid = originalTxId,
168+
satsPerVByte = highFeeRate,
169+
)
170+
171+
assertFalse(replacementTxId.isEmpty(), "Replacement transaction ID should not be empty")
172+
assertTrue(
173+
replacementTxId != originalTxId,
174+
"Replacement transaction ID should be different from original"
175+
)
176+
println("Fee bumped successfully! Replacement transaction ID: $replacementTxId")
177+
178+
// Mine a block to confirm the replacement transaction
179+
println("Mining 1 block to confirm the replacement transaction")
180+
coreService.blocktank.regtestMine(1u)
181+
println("Block mined successfully")
182+
183+
// Wait for the block to be processed
184+
println("Waiting 10 seconds for block to be processed")
185+
delay(10_000)
186+
println("Wait completed")
187+
188+
// Sync the wallet to update balances
189+
println("Syncing wallet to update balances after fee bump")
190+
lightningService.sync()
191+
println("Wallet sync complete")
192+
193+
// Verify the balance has been updated (should be less due to the higher fee)
194+
val updatedBalances = lightningService.balances
195+
assertNotNull(updatedBalances, "Updated balances should not be null")
196+
val finalBalance = updatedBalances.totalOnchainBalanceSats
197+
println("Final balance after fee bump: $finalBalance sats")
198+
199+
// The final balance should be less than the initial balance due to the sent amount and fees
200+
assertTrue(finalBalance < totalBalance, "Final balance should be less than initial balance")
201+
println("✓ RBF fee bump test completed successfully")
202+
}
203+
204+
@Test
205+
fun testAccelerateByCpfp() = runBlocking {
206+
println("Starting accelerate by CPFP test")
207+
208+
// Create a new wallet using WalletRepo
209+
println("Creating new wallet")
210+
walletRepo.createWallet(bip39Passphrase = null)
211+
lightningService.setup(walletIndex = walletIndex)
212+
213+
println("Starting lightning node")
214+
lightningService.start()
215+
println("Lightning node started successfully")
216+
217+
// Test wallet sync
218+
println("Syncing wallet")
219+
lightningService.sync()
220+
println("Wallet sync complete")
221+
222+
// Generate an address to receive funds
223+
println("Generating deposit address")
224+
val depositAddress = lightningService.newAddress()
225+
println("Deposit address: $depositAddress")
226+
227+
// Simulate receiving a transaction with low fees (this represents someone sending us funds with insufficient fees)
228+
val incomingAmount = 100_000uL // 100,000 sats incoming
229+
println("Simulating incoming transaction with low fees: $incomingAmount sats")
230+
231+
// Use blocktank to send us funds with very low fees (simulating a stuck incoming transaction)
232+
// In a real scenario, this would be someone else sending us funds with insufficient fees
233+
val stuckIncomingTxId = coreService.blocktank.regtestDeposit(
234+
address = depositAddress,
235+
amountSat = incomingAmount
236+
)
237+
assertFalse(
238+
stuckIncomingTxId.isEmpty(),
239+
"Stuck incoming transaction ID should not be empty"
240+
)
241+
println("Stuck incoming transaction ID: $stuckIncomingTxId")
242+
243+
println("Waiting 20 seconds")
244+
delay(20_000)
245+
println("Wait completed")
246+
247+
// Sync to see the incoming transaction
248+
println("Syncing wallet to detect incoming transaction")
249+
lightningService.sync()
250+
println("Wallet sync complete")
251+
252+
// Check that we can see the balance from the incoming transaction
253+
val balances = lightningService.balances
254+
assertNotNull(balances, "Balances should not be null")
255+
val currentBalance = balances.totalOnchainBalanceSats
256+
println("Current balance: $currentBalance sats")
257+
258+
// The balance should reflect the incoming amount
259+
assertTrue(currentBalance > 0uL, "Should have balance from incoming transaction")
260+
assertEquals(incomingAmount, currentBalance, "Balance should equal incoming amount")
261+
262+
// Now use CPFP to spend from the incoming transaction with high fees
263+
// This demonstrates using CPFP to quickly move received funds
264+
val highFeeRate = 20u // 20 sat/vbyte (very high for fast confirmation)
265+
println("Using CPFP to quickly spend from incoming transaction $stuckIncomingTxId with $highFeeRate sat/vbyte")
266+
267+
// Generate a destination address for the CPFP transaction (where we'll send the funds)
268+
println("Generating destination address for CPFP child transaction")
269+
val cpfpDestinationAddress = lightningService.newAddress()
270+
println("CPFP destination address: $cpfpDestinationAddress")
271+
272+
val childTxId = lightningService.accelerateByCpfp(
273+
txid = stuckIncomingTxId,
274+
satsPerVByte = highFeeRate,
275+
destinationAddress = cpfpDestinationAddress,
276+
)
277+
278+
assertFalse(childTxId.isEmpty(), "CPFP child transaction ID should not be empty")
279+
assertTrue(
280+
childTxId != stuckIncomingTxId,
281+
"Child transaction ID should be different from parent"
282+
)
283+
println("CPFP child transaction created successfully! Child transaction ID: $childTxId")
284+
println("This child transaction spends from the parent and pays high fees for fast confirmation")
285+
286+
// Mine blocks to confirm the CPFP child transaction
287+
println("Mining 2 blocks to confirm the CPFP child transaction")
288+
coreService.blocktank.regtestMine(2u)
289+
println("Blocks mined successfully - both transactions should now be confirmed")
290+
291+
// Wait for the blocks to be processed
292+
println("Waiting 10 seconds for blocks to be processed")
293+
delay(10_000)
294+
println("Wait completed")
295+
296+
// Sync the wallet to update balances
297+
println("Syncing wallet to update balances after CPFP confirmation")
298+
lightningService.sync()
299+
println("Wallet sync complete")
300+
301+
// Verify the final balance
302+
val finalBalances = lightningService.balances
303+
assertNotNull(finalBalances, "Final balances should not be null")
304+
val finalBalance = finalBalances.totalOnchainBalanceSats
305+
println("Final confirmed balance after CPFP: $finalBalance sats")
306+
307+
// We should have received the incoming amount minus the CPFP fees
308+
// The exact amount depends on the fee calculation, but it should be positive and less than the incoming amount
309+
assertTrue(finalBalance > 0uL, "Should have positive balance after CPFP")
310+
assertTrue(
311+
finalBalance < incomingAmount,
312+
"Final balance should be less than incoming amount due to CPFP fees"
313+
)
314+
315+
println("✓ CPFP test completed successfully")
316+
println("Successfully used CPFP to quickly spend from an incoming transaction")
317+
}
318+
}

0 commit comments

Comments
 (0)