Skip to content

Commit 5fa7582

Browse files
authored
Merge branch 'master' into fix/transfer-activity-explore-button
2 parents a3543fa + f1769a8 commit 5fa7582

83 files changed

Lines changed: 4772 additions & 455 deletions

File tree

Some content is hidden

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

.github/workflows/e2e.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ jobs:
126126
E2E: true
127127
E2E_BACKEND: network
128128
GEO: false
129+
TREZOR_BRIDGE: true
130+
TREZOR_BRIDGE_URL: http://10.0.2.2:21325
129131
run: ./gradlew assembleDevDebug
130132

131133
- name: Rename APK
@@ -320,6 +322,7 @@ jobs:
320322
shard:
321323
- { name: multi_address_2_regtest, grep: "@multi_address_2" }
322324
- { name: pubky_paykit, grep: "@pubky" }
325+
- { name: hardware_wallet, grep: "@hardware_wallet" }
323326

324327
name: e2e-tests-staging - ${{ matrix.shard.name }}
325328

@@ -390,6 +393,13 @@ jobs:
390393
working-directory: bitkit-e2e-tests
391394
run: npm ci
392395

396+
- name: Pull Trezor emulator image
397+
if: matrix.shard.name == 'hardware_wallet'
398+
working-directory: bitkit-e2e-tests
399+
run: |
400+
cd docker
401+
docker compose --profile trezor pull --quiet trezor-user-env
402+
393403
- name: Run E2E Tests 1 (${{ matrix.shard.name }})
394404
continue-on-error: true
395405
id: test1

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
184184
- ALWAYS ensure a method exist before calling it
185185
- ALWAYS remove unused code after refactors
186186
- ALWAYS follow Material3 design guidelines for UI components
187+
- When building from a Figma frame, reuse only scaffolding (sheet host, `SheetTopBar`, buttons, typography); NEVER swap a design-specific illustration/animation for a lookalike. Export the frame's assets via the Figma MCP and read animation timing/easing/direction from prototype reactions (`use_figma``node.reactions`)
187188
- ALWAYS ensure proper error handling in coroutines
188189
- ALWAYS acknowledge datastore async operations run synchronously in a suspend context
189190
- NEVER use `runBlocking` in suspend functions
@@ -215,11 +216,14 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
215216
- USE `docs/` as target dir of saved files when asked to create documentation for new features
216217
- NEVER write code in the documentation files
217218
- NEVER add code comments to private functions, classes, etc
219+
- ALWAYS use `/** */` to document constants
218220
- ALWAYS use `_uiState.update { }`, NEVER use `_stateFlow.value =`
219221
- ALWAYS add the warranted changes in unit tests to keep the unit tests succeeding
220222
- ALWAYS follow the patterns of the existing code in `app/src/test` when writing new unit tests
221223
- ALWAYS be mindful of thread safety when working with mutable lists & state
222224
- ALWAYS split screen composables into parent accepting viewmodel + inner private child accepting state and callbacks `Content()`
225+
- ALWAYS preview an in-sheet screen as `BottomSheetPreview { Content(modifier = Modifier.sheetHeight()) }`, passing the host's `SheetSize` when it isn't the default `LARGE`; see `SendErrorScreen.kt`
226+
- ALWAYS write Compose `testTag`s in PascalCase (e.g. `HwPairedFinish`), never snake_case
223227
- ALWAYS name lambda parameters in a composable function using present tense, NEVER use past tense
224228
- ALWAYS use `whenever { mock.suspendCall() }` for suspend stubs if not inside `test{}` fn blocks
225229
- ALWAYS use `whenever(mock.call())` for non-suspend stubs and for suspend stubs if inside `test{}` fn blocks

app/src/main/java/to/bitkit/data/HwWalletStore.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import kotlinx.coroutines.withContext
1111
import kotlinx.serialization.Serializable
1212
import to.bitkit.data.serializers.HwWalletDataSerializer
1313
import to.bitkit.di.IoDispatcher
14-
import to.bitkit.repositories.KnownDevice
14+
import to.bitkit.models.KnownDevice
1515
import javax.inject.Inject
1616
import javax.inject.Singleton
1717

app/src/main/java/to/bitkit/data/dao/TransferDao.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.Flow
1010
import to.bitkit.data.entities.TransferEntity
1111

1212
@Dao
13+
@Suppress("TooManyFunctions")
1314
interface TransferDao {
1415
@Insert(onConflict = OnConflictStrategy.REPLACE)
1516
suspend fun insert(transfer: TransferEntity)
@@ -35,6 +36,9 @@ interface TransferDao {
3536
@Query("SELECT * FROM transfers WHERE id = :id LIMIT 1")
3637
suspend fun getById(id: String): TransferEntity?
3738

39+
@Query("SELECT * FROM transfers WHERE fundingTxId = :fundingTxId LIMIT 1")
40+
suspend fun getByFundingTxId(fundingTxId: String): TransferEntity?
41+
3842
@Query("UPDATE transfers SET isSettled = 1, settledAt = :settledAt WHERE id = :id")
3943
suspend fun markSettled(id: String, settledAt: Long)
4044

app/src/main/java/to/bitkit/env/Env.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,9 @@ object Defaults {
263263
/** Recommended transaction base fee in sats */
264264
const val recommendedBaseFee = 256u
265265

266+
/** Fallback fee percentage used when fee estimates are temporarily unavailable. */
267+
const val fallbackFeePercent = 0.1
268+
266269
/**
267270
* Minimum value in sats for an output. Outputs below the dust limit may not be processed because the fees
268271
* required to include them in a block would be greater than the value of the transaction itself.

app/src/main/java/to/bitkit/ext/Context.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ val Context.usbManager: UsbManager
4646
val Context.bluetoothManager: BluetoothManager
4747
get() = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
4848

49+
val Context.isBluetoothEnabled: Boolean
50+
get() = bluetoothManager.adapter?.isEnabled == true
51+
4952
val Context.powerManager: PowerManager
5053
get() = getSystemService(Context.POWER_SERVICE) as PowerManager
5154

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package to.bitkit.ext
2+
3+
import com.synonym.bitkitcore.TrezorTransportType
4+
import to.bitkit.models.TransportType
5+
6+
fun TrezorTransportType.toTransportType(): TransportType = when (this) {
7+
TrezorTransportType.BLUETOOTH -> TransportType.BLUETOOTH
8+
TrezorTransportType.USB -> TransportType.USB
9+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package to.bitkit.models
2+
3+
import androidx.compose.runtime.Immutable
4+
import androidx.compose.runtime.Stable
5+
import com.synonym.bitkitcore.AccountType
6+
import com.synonym.bitkitcore.Activity
7+
import com.synonym.bitkitcore.AddressType
8+
import kotlinx.collections.immutable.ImmutableList
9+
import kotlinx.collections.immutable.ImmutableSet
10+
import kotlinx.collections.immutable.persistentSetOf
11+
import kotlinx.serialization.Serializable
12+
13+
/** A paired hardware wallet tracked as a watch-only balance. */
14+
@Stable
15+
data class HwWallet(
16+
val id: String,
17+
val name: String,
18+
val model: String?,
19+
val transportType: TransportType,
20+
val isConnected: Boolean,
21+
val balanceSats: ULong,
22+
val activities: ImmutableList<Activity>,
23+
val fundingBalanceSats: ULong = balanceSats,
24+
val deviceIds: ImmutableSet<String> = persistentSetOf(id),
25+
)
26+
27+
/** Serializable per-device balance snapshot carried by [BalanceState]. */
28+
@Immutable
29+
@Serializable
30+
data class HwWalletBalance(
31+
val id: String,
32+
val sats: ULong,
33+
)
34+
35+
/** A newly detected inbound transaction to a watched hardware wallet. */
36+
@Immutable
37+
data class HwWalletReceivedTx(
38+
val txid: String,
39+
val sats: ULong,
40+
)
41+
42+
sealed interface HwFundingAccount {
43+
val vendor: HwWalletVendor
44+
val xpub: String
45+
val addressType: HwFundingAddressType
46+
val accountType: AccountType
47+
val balanceSats: ULong
48+
49+
data class Trezor(
50+
override val xpub: String,
51+
override val addressType: HwFundingAddressType,
52+
override val balanceSats: ULong,
53+
) : HwFundingAccount {
54+
override val vendor: HwWalletVendor = HwWalletVendor.TREZOR
55+
override val accountType: AccountType
56+
get() = addressType.accountType
57+
}
58+
}
59+
60+
data class HwFundingTransaction(
61+
val psbt: String,
62+
val miningFeeSats: ULong,
63+
val feeRate: Float,
64+
val totalSpent: ULong,
65+
val satsPerVByte: ULong,
66+
)
67+
68+
data class HwFundingBroadcastResult(
69+
val txId: String,
70+
val miningFeeSats: ULong,
71+
val feeRate: ULong,
72+
val totalSpent: ULong,
73+
)
74+
75+
enum class HwWalletVendor {
76+
TREZOR,
77+
}
78+
79+
enum class HwFundingAddressType(
80+
val addressType: AddressType,
81+
) {
82+
LEGACY(AddressType.P2PKH),
83+
NESTED_SEGWIT(AddressType.P2SH),
84+
NATIVE_SEGWIT(AddressType.P2WPKH),
85+
TAPROOT(AddressType.P2TR);
86+
87+
val settingsKey: String
88+
get() = addressType.toSettingsString()
89+
90+
val accountType: AccountType
91+
get() = addressType.toAccountType()
92+
93+
companion object {
94+
val DEFAULT: HwFundingAddressType = entries.first { it.addressType == DEFAULT_ADDRESS_TYPE }
95+
}
96+
}
97+
98+
fun HwWallet.toBalance() = HwWalletBalance(id = id, sats = balanceSats)

app/src/main/java/to/bitkit/models/HwWallet.kt

Lines changed: 0 additions & 39 deletions
This file was deleted.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package to.bitkit.models
2+
3+
import androidx.compose.runtime.Immutable
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
@Immutable
8+
data class KnownDevice(
9+
val id: String,
10+
val name: String?,
11+
val path: String,
12+
val transportType: TransportType,
13+
val label: String?,
14+
val model: String?,
15+
val lastConnectedAt: Long,
16+
/** Account-level extended public keys per address type. */
17+
val xpubs: Map<String, String> = emptyMap(),
18+
/** Bitkit-side funds label set by the user while pairing; null until renamed within Bitkit. */
19+
val customLabel: String? = null,
20+
/** Stable app-owned id for future wallet-scoped hardware activity metadata. */
21+
val walletId: String = "",
22+
)

0 commit comments

Comments
 (0)