Skip to content

Commit d1f3231

Browse files
committed
feat: add emulated trezor service
1 parent e951e8a commit d1f3231

8 files changed

Lines changed: 586 additions & 35 deletions

File tree

app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ val bcp47Locales = listOf(
4848
)
4949
val e2eBackendEnv = System.getenv("E2E_BACKEND") ?: "local"
5050
val e2eHomegateUrlEnv = System.getenv("E2E_HOMEGATE_URL") ?: "http://127.0.0.1:6288"
51+
val emulatedTrezorEnv = System.getenv("EMULATED_TREZOR")?.toBoolean()?.toString() ?: "false"
5152

5253
android {
5354
namespace = "to.bitkit"
@@ -65,6 +66,7 @@ android {
6566
buildConfigField("boolean", "E2E", System.getenv("E2E")?.toBoolean()?.toString() ?: "false")
6667
buildConfigField("String", "E2E_BACKEND", "\"$e2eBackendEnv\"")
6768
buildConfigField("String", "E2E_HOMEGATE_URL", "\"$e2eHomegateUrlEnv\"")
69+
buildConfigField("boolean", "EMULATED_TREZOR", emulatedTrezorEnv)
6870
buildConfigField("boolean", "GEO", System.getenv("GEO")?.toBoolean()?.toString() ?: "true")
6971
buildConfigField("String", "LOCALES", "\"${bcp47Locales.joinToString(",")}\"")
7072
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package to.bitkit.di
2+
3+
import dagger.Module
4+
import dagger.Provides
5+
import dagger.hilt.InstallIn
6+
import dagger.hilt.components.SingletonComponent
7+
import to.bitkit.BuildConfig
8+
import to.bitkit.services.EmulatedTrezorService
9+
import to.bitkit.services.TrezorService
10+
import to.bitkit.services.TrezorServiceApi
11+
import javax.inject.Singleton
12+
13+
@Module
14+
@InstallIn(SingletonComponent::class)
15+
object TrezorModule {
16+
17+
@Provides
18+
@Singleton
19+
fun provideTrezorService(
20+
realService: TrezorService,
21+
emulatedService: EmulatedTrezorService,
22+
): TrezorServiceApi {
23+
return if (BuildConfig.EMULATED_TREZOR) emulatedService else realService
24+
}
25+
}

app/src/main/java/to/bitkit/repositories/TrezorRepo.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import to.bitkit.di.IoDispatcher
3535
import to.bitkit.env.Env
3636
import to.bitkit.models.toCoreNetwork
3737
import to.bitkit.services.TrezorDebugLog
38-
import to.bitkit.services.TrezorService
38+
import to.bitkit.services.TrezorServiceApi
3939
import to.bitkit.services.TrezorTransport
4040
import to.bitkit.utils.AppError
4141
import to.bitkit.utils.Logger
@@ -48,7 +48,7 @@ import com.synonym.bitkitcore.Network as BitkitCoreNetwork
4848
@Singleton
4949
class TrezorRepo @Inject constructor(
5050
@ApplicationContext private val context: Context,
51-
private val trezorService: TrezorService,
51+
private val trezorService: TrezorServiceApi,
5252
private val trezorTransport: TrezorTransport,
5353
private val trezorStore: TrezorStore,
5454
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
package to.bitkit.services
2+
3+
import com.synonym.bitkitcore.AccountAddresses
4+
import com.synonym.bitkitcore.AccountInfoResult
5+
import com.synonym.bitkitcore.AccountType
6+
import com.synonym.bitkitcore.AccountUtxo
7+
import com.synonym.bitkitcore.ComposeAccount
8+
import com.synonym.bitkitcore.ComposeParams
9+
import com.synonym.bitkitcore.ComposeResult
10+
import com.synonym.bitkitcore.HistoryTransaction
11+
import com.synonym.bitkitcore.SingleAddressInfoResult
12+
import com.synonym.bitkitcore.TransactionHistoryResult
13+
import com.synonym.bitkitcore.TrezorAddressResponse
14+
import com.synonym.bitkitcore.TrezorCoinType
15+
import com.synonym.bitkitcore.TrezorDeviceInfo
16+
import com.synonym.bitkitcore.TrezorFeatures
17+
import com.synonym.bitkitcore.TrezorPublicKeyResponse
18+
import com.synonym.bitkitcore.TrezorScriptType
19+
import com.synonym.bitkitcore.TrezorSignedMessageResponse
20+
import com.synonym.bitkitcore.TrezorSignedTx
21+
import com.synonym.bitkitcore.TrezorTransportType
22+
import com.synonym.bitkitcore.TxDirection
23+
import com.synonym.bitkitcore.WalletBalance
24+
import to.bitkit.utils.AppError
25+
import javax.inject.Inject
26+
import javax.inject.Singleton
27+
import com.synonym.bitkitcore.Network as BitkitCoreNetwork
28+
29+
@Suppress("MagicNumber", "TooManyFunctions")
30+
@Singleton
31+
class EmulatedTrezorService @Inject constructor() : TrezorServiceApi {
32+
companion object {
33+
private const val DEVICE_ID = "emulated-trezor-001"
34+
private const val DEVICE_PATH = "emulated://trezor/001"
35+
private const val DEVICE_NAME = "Emulated Trezor"
36+
private const val DEVICE_MODEL = "Safe 5"
37+
private const val DEVICE_LABEL = "Emulated Trezor"
38+
private const val FINGERPRINT = "e7e1f001"
39+
private const val ACCOUNT_PATH = "m/84'/0'/0'"
40+
private const val ADDRESS_PATH = "m/84'/0'/0'/0/0"
41+
private const val MAINNET_ADDRESS = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080"
42+
private const val TESTNET_ADDRESS = "tb1qfmkh9r5daqmt66h55m08ggx7lsvx4dfkcvvhlf"
43+
private const val REGTEST_ADDRESS = "bcrt1qfmkh9r5daqmt66h55m08ggx7lsvx4dfk6v5gxy"
44+
private const val XPUB =
45+
"xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKrhko4egpiMZbpiaQL2jkwSB1icqY" +
46+
"h2cfDfVxdx4df189oLKnC5fSwqPfgyP3hooxujYzAu3fDVmz"
47+
private const val PUBLIC_KEY =
48+
"02a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
49+
private const val CHAIN_CODE =
50+
"f1e2d3c4b5a6f1e2d3c4b5a6f1e2d3c4b5a6f1e2d3c4b5a6f1e2d3c4b5a6f1e2"
51+
private const val TXID =
52+
"c4d5e6f7a8b9c4d5e6f7a8b9c4d5e6f7a8b9c4d5e6f7a8b9c4d5e6f7a8b9c4d5"
53+
private const val SIGNATURE_PREFIX = "emulated-signature:"
54+
}
55+
56+
@Volatile
57+
private var initialized = false
58+
59+
@Volatile
60+
private var connected = false
61+
62+
private val device = TrezorDeviceInfo(
63+
id = DEVICE_ID,
64+
transportType = TrezorTransportType.USB,
65+
name = DEVICE_NAME,
66+
path = DEVICE_PATH,
67+
label = DEVICE_LABEL,
68+
model = DEVICE_MODEL,
69+
isBootloader = false,
70+
)
71+
72+
private val features = TrezorFeatures(
73+
vendor = "trezor.io",
74+
model = DEVICE_MODEL,
75+
label = DEVICE_LABEL,
76+
deviceId = DEVICE_ID,
77+
majorVersion = 2u,
78+
minorVersion = 8u,
79+
patchVersion = 1u,
80+
pinProtection = true,
81+
passphraseProtection = false,
82+
initialized = true,
83+
needsBackup = false,
84+
)
85+
86+
override suspend fun initialize(credentialPath: String?) {
87+
initialized = true
88+
}
89+
90+
override suspend fun isInitialized(): Boolean = initialized
91+
92+
override suspend fun scan(): List<TrezorDeviceInfo> = listOf(device)
93+
94+
override suspend fun listDevices(): List<TrezorDeviceInfo> = listOf(device)
95+
96+
override suspend fun connect(deviceId: String): TrezorFeatures {
97+
if (deviceId != DEVICE_ID) {
98+
throw AppError("Unknown emulated Trezor device '$deviceId'")
99+
}
100+
initialized = true
101+
connected = true
102+
return features
103+
}
104+
105+
override suspend fun isConnected(): Boolean = connected
106+
107+
override suspend fun getAddress(
108+
path: String,
109+
coin: TrezorCoinType?,
110+
showOnTrezor: Boolean,
111+
scriptType: TrezorScriptType?,
112+
): TrezorAddressResponse {
113+
ensureConnected()
114+
return TrezorAddressResponse(
115+
address = addressFor(coin),
116+
path = path,
117+
)
118+
}
119+
120+
override suspend fun getPublicKey(
121+
path: String,
122+
coin: TrezorCoinType?,
123+
showOnTrezor: Boolean,
124+
): TrezorPublicKeyResponse {
125+
ensureConnected()
126+
return TrezorPublicKeyResponse(
127+
xpub = XPUB,
128+
path = path,
129+
publicKey = PUBLIC_KEY,
130+
chainCode = CHAIN_CODE,
131+
fingerprint = 0x1234u,
132+
depth = 3u,
133+
rootFingerprint = 0x5678u,
134+
)
135+
}
136+
137+
override suspend fun disconnect() {
138+
connected = false
139+
}
140+
141+
override suspend fun getConnectedDevice(): TrezorDeviceInfo? = if (connected) device else null
142+
143+
override suspend fun signMessage(
144+
path: String,
145+
message: String,
146+
coin: TrezorCoinType?,
147+
): TrezorSignedMessageResponse {
148+
ensureConnected()
149+
return TrezorSignedMessageResponse(
150+
address = addressFor(coin),
151+
signature = signatureFor(message),
152+
)
153+
}
154+
155+
override suspend fun verifyMessage(
156+
address: String,
157+
signature: String,
158+
message: String,
159+
coin: TrezorCoinType?,
160+
): Boolean {
161+
ensureConnected()
162+
return address == addressFor(coin) && signature == signatureFor(message)
163+
}
164+
165+
override suspend fun clearCredentials(deviceId: String) {
166+
if (deviceId == DEVICE_ID) {
167+
initialized = false
168+
}
169+
}
170+
171+
override suspend fun composeTransaction(params: ComposeParams): List<ComposeResult> {
172+
ensureConnected()
173+
return listOf(
174+
ComposeResult.Success(
175+
psbt = "cHNidP8BAHECAAAAAQ==",
176+
fee = 1_000uL,
177+
feeRate = 5.0f,
178+
totalSpent = 51_000uL,
179+
)
180+
)
181+
}
182+
183+
override suspend fun signTxFromPsbt(psbtBase64: String, network: TrezorCoinType?): TrezorSignedTx {
184+
ensureConnected()
185+
return TrezorSignedTx(
186+
signatures = listOf("304402200a0b0c0d"),
187+
serializedTx = "0200000001a1b2c3d4deadbeef",
188+
txid = TXID,
189+
)
190+
}
191+
192+
override suspend fun getDeviceFingerprint(): String {
193+
ensureConnected()
194+
return FINGERPRINT
195+
}
196+
197+
override suspend fun broadcastRawTx(serializedTx: String, electrumUrl: String): String {
198+
ensureConnected()
199+
return TXID
200+
}
201+
202+
override suspend fun getTransactionHistory(
203+
extendedKey: String,
204+
electrumUrl: String,
205+
network: BitkitCoreNetwork?,
206+
scriptType: AccountType?,
207+
): TransactionHistoryResult = TransactionHistoryResult(
208+
transactions = listOf(
209+
HistoryTransaction(
210+
txid = TXID,
211+
received = 100_000uL,
212+
sent = 0uL,
213+
net = 100_000L,
214+
fee = null,
215+
amount = 100_000uL,
216+
direction = TxDirection.RECEIVED,
217+
blockHeight = 849_990u,
218+
timestamp = 1_700_000_000uL,
219+
confirmations = 10u,
220+
)
221+
),
222+
balance = WalletBalance(
223+
confirmed = 100_000uL,
224+
immature = 0uL,
225+
trustedPending = 0uL,
226+
untrustedPending = 0uL,
227+
spendable = 100_000uL,
228+
total = 100_000uL,
229+
),
230+
txCount = 1u,
231+
blockHeight = 850_000u,
232+
accountType = scriptType ?: AccountType.NATIVE_SEGWIT,
233+
)
234+
235+
override suspend fun getAccountInfo(
236+
extendedKey: String,
237+
electrumUrl: String,
238+
network: BitkitCoreNetwork?,
239+
gapLimit: UInt?,
240+
scriptType: AccountType?,
241+
): AccountInfoResult {
242+
val utxo = AccountUtxo(
243+
txid = TXID,
244+
vout = 0u,
245+
amount = 100_000uL,
246+
blockHeight = 849_990u,
247+
address = addressFor(network),
248+
path = ADDRESS_PATH,
249+
confirmations = 10u,
250+
coinbase = false,
251+
own = true,
252+
required = null,
253+
)
254+
return AccountInfoResult(
255+
account = ComposeAccount(
256+
path = ACCOUNT_PATH,
257+
addresses = AccountAddresses(
258+
used = emptyList(),
259+
unused = emptyList(),
260+
change = emptyList(),
261+
),
262+
utxo = listOf(utxo),
263+
),
264+
balance = 100_000uL,
265+
utxoCount = 1u,
266+
accountType = scriptType ?: AccountType.NATIVE_SEGWIT,
267+
blockHeight = 850_000u,
268+
)
269+
}
270+
271+
override suspend fun getAddressInfo(
272+
address: String,
273+
electrumUrl: String,
274+
network: BitkitCoreNetwork?,
275+
): SingleAddressInfoResult {
276+
val utxo = AccountUtxo(
277+
txid = TXID,
278+
vout = 0u,
279+
amount = 100_000uL,
280+
blockHeight = 849_990u,
281+
address = address,
282+
path = ADDRESS_PATH,
283+
confirmations = 10u,
284+
coinbase = false,
285+
own = true,
286+
required = null,
287+
)
288+
return SingleAddressInfoResult(
289+
address = address,
290+
balance = 100_000uL,
291+
utxos = listOf(utxo),
292+
transfers = 1u,
293+
blockHeight = 850_000u,
294+
)
295+
}
296+
297+
private fun ensureConnected() {
298+
if (!connected) {
299+
throw AppError("Emulated Trezor is not connected")
300+
}
301+
}
302+
303+
private fun addressFor(coin: TrezorCoinType?): String = when (coin) {
304+
TrezorCoinType.TESTNET,
305+
TrezorCoinType.SIGNET,
306+
-> TESTNET_ADDRESS
307+
TrezorCoinType.REGTEST -> REGTEST_ADDRESS
308+
else -> MAINNET_ADDRESS
309+
}
310+
311+
private fun addressFor(network: BitkitCoreNetwork?): String = when (network) {
312+
BitkitCoreNetwork.TESTNET,
313+
BitkitCoreNetwork.TESTNET4,
314+
BitkitCoreNetwork.SIGNET,
315+
-> TESTNET_ADDRESS
316+
BitkitCoreNetwork.REGTEST -> REGTEST_ADDRESS
317+
else -> MAINNET_ADDRESS
318+
}
319+
320+
private fun signatureFor(message: String): String = "$SIGNATURE_PREFIX${message.hashCode()}"
321+
}

0 commit comments

Comments
 (0)