bitbox-api 0.12.0

A library to interact with BitBox hardware wallets
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
// SPDX-License-Identifier: Apache-2.0

use std::str::FromStr;

mod connect;
mod localstorage;
mod noise;
mod types;

use wasm_bindgen::prelude::*;

use thiserror::Error;

use enum_assoc::Assoc;

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

#[derive(Assoc)]
#[func(pub fn js_code(&self) -> String)]
#[derive(Error, Debug)]
pub enum JavascriptError {
    #[error("Unknown Javascript error")]
    #[assoc(js_code = "unknown-js".into())]
    Unknown,
    #[error(
        "Could not open device. It might already have an open connection to another app. If so, please close the other app first."
    )]
    #[assoc(js_code = "could-not-open".into())]
    CouldNotOpenWebHID,
    #[error("Could not open device. {0}")]
    #[assoc(js_code = "could-not-open".into())]
    CouldNotOpenBridge(String),
    #[error("connection aborted by user")]
    #[assoc(js_code="user-abort".into())]
    UserAbort,
    #[error("{0}")]
    #[cfg_attr(feature = "wasm", assoc(js_code = _0.js_code().into()))]
    BitBox(#[from] crate::error::Error),
    #[error("invalid JavaScript type: {0}")]
    #[assoc(js_code = "invalid-type".into())]
    InvalidType(&'static str),
    #[error("invalid JavaScript type: {0}")]
    #[assoc(js_code = "invalid-type".into())]
    Foo(String),
    #[error("PSBT parse error: {0}")]
    #[assoc(js_code = "psbt-parse".into())]
    PsbtParseError(#[from] bitcoin::psbt::PsbtParseError),
    #[error("Chain ID too large and would overflow in the computation of the `v` signature value: {chain_id}")]
    #[assoc(js_code = "chain-id-too-large".into())]
    ChainIDTooLarge { chain_id: u64 },
}

impl From<JavascriptError> for JsValue {
    fn from(val: JavascriptError) -> Self {
        let obj = js_sys::Object::new();

        js_sys::Reflect::set(&obj, &"code".into(), &val.js_code().into()).unwrap();
        js_sys::Reflect::set(&obj, &"message".into(), &val.to_string().into()).unwrap();

        obj.into()
    }
}

/// Run any exception raised by this library through this function to get a typed error.
///
/// Example:
/// ```JavaScript
/// try { ... }
/// catch (err) {
///   const typedErr: Error = bitbox.ensureError(err);
///   // Handle error by checking the error code, displaying the error message, etc.
/// }
///
/// See also: isUserAbort().
#[wasm_bindgen(js_name = ensureError)]
pub fn ensure_error(err: JsValue) -> types::TsError {
    let code = js_sys::Reflect::get(&err, &"code".into());
    let message = js_sys::Reflect::get(&err, &"message".into());
    match (code, message) {
        (Ok(code), Ok(message)) if code.is_string() && message.is_string() => err.into(),
        _ => {
            let js_result: JsValue = JavascriptError::Unknown.into();
            js_sys::Reflect::set(&js_result, &"err".into(), &err).unwrap();
            js_result.into()
        }
    }
}

/// Returns true if the user cancelled an operation.
#[wasm_bindgen(js_name = isUserAbort)]
pub fn is_user_abort(err: types::TsError) -> bool {
    match js_sys::Reflect::get(&err, &"code".into()) {
        Ok(code) => matches!(
            code.as_string().as_deref(),
            Some("user-abort" | "bitbox-user-abort")
        ),
        _ => false,
    }
}

#[wasm_bindgen(js_name = ethIdentifyCase)]
pub fn eth_identify_case(recipient_address: &str) -> types::TsEthAddressCase {
    crate::eth::eth_identify_case(recipient_address).into()
}

#[wasm_bindgen(raw_module = "./webhid.js")]
extern "C" {
    async fn jsSleep(millis: f64);
}

struct WasmRuntime;

#[async_trait::async_trait(?Send)]
impl crate::runtime::Runtime for WasmRuntime {
    async fn sleep(dur: std::time::Duration) {
        jsSleep(dur.as_millis() as _).await
    }
}

/// BitBox client. Instantiate it using `bitbox02ConnectAuto()`.
#[wasm_bindgen]
pub struct BitBox {
    device: crate::BitBox<WasmRuntime>,
    close_function: js_sys::Function,
}

/// BitBox in the pairing state. Use `getPairingCode()` to display the pairing code to the user and
/// `waitConfirm()` to proceed to the paired state.
#[wasm_bindgen]
pub struct PairingBitBox {
    device: crate::PairingBitBox<WasmRuntime>,
    close_function: js_sys::Function,
}

/// Paired BitBox. This is where you can invoke most API functions like getting xpubs, displaying
/// receive addresses, etc.
#[wasm_bindgen]
pub struct PairedBitBox {
    device: crate::PairedBitBox<WasmRuntime>,
    close_function: js_sys::Function,
}

#[wasm_bindgen]
impl BitBox {
    /// Invokes the device unlock and pairing. After this, stop using this instance and continue
    /// with the returned instance of type `PairingBitBox`.
    #[wasm_bindgen(js_name = unlockAndPair)]
    pub async fn unlock_and_pair(self) -> Result<PairingBitBox, JavascriptError> {
        Ok(PairingBitBox {
            device: self.device.unlock_and_pair().await?,
            close_function: self.close_function,
        })
    }
}

/// BitBox in the pairing state. Use `getPairingCode()` to display the pairing code to the user and
/// `waitConfirm()` to proceed to the paired state.
#[wasm_bindgen]
impl PairingBitBox {
    /// If a pairing code confirmation is required, this returns the pairing code. You must display
    /// it to the user and then call `waitConfirm()` to wait until the user confirms the code on
    /// the BitBox.
    ///
    /// If the BitBox was paired before and the pairing was persisted, the pairing step is
    /// skipped. In this case, `undefined` is returned. Also in this case, call `waitConfirm()` to
    /// establish the encrypted connection.
    #[wasm_bindgen(js_name = getPairingCode)]
    pub fn get_pairing_code(&self) -> Option<String> {
        self.device.get_pairing_code()
    }

    /// Proceed to the paired state. After this, stop using this instance and continue with the
    /// returned instance of type `PairedBitBox`.
    #[wasm_bindgen(js_name = waitConfirm)]
    pub async fn wait_confirm(self) -> Result<PairedBitBox, JavascriptError> {
        Ok(PairedBitBox {
            device: self.device.wait_confirm().await?,
            close_function: self.close_function,
        })
    }
}

fn compute_v(chain_id: u64, rec_id: u8) -> Option<u64> {
    let v_offset: u64 = chain_id.checked_mul(2)?.checked_add(8)?;
    (rec_id as u64 + 27).checked_add(v_offset)
}

/// Paired BitBox. This is where you can invoke most API functions like getting xpubs, displaying
/// receive addresses, etc.
#[wasm_bindgen]
impl PairedBitBox {
    /// Closes the BitBox connection. This also invokes the `on_close_cb` callback which was
    /// provided to the connect method creating the connection.
    #[wasm_bindgen(js_name = close)]
    pub fn close(self) {
        self.close_function.call0(&JsValue::NULL).unwrap();
    }

    #[wasm_bindgen(js_name = deviceInfo)]
    pub async fn device_info(&self) -> Result<types::TsDeviceInfo, JavascriptError> {
        let result = self.device.device_info().await?;
        Ok(serde_wasm_bindgen::to_value(&result).unwrap().into())
    }

    /// Returns which product we are connected to.
    #[wasm_bindgen(js_name = product)]
    pub fn product(&self) -> types::TsProduct {
        match self.device.product() {
            crate::Product::Unknown => JsValue::from_str("unknown").into(),
            crate::Product::BitBox02Multi => JsValue::from_str("bitbox02-multi").into(),
            crate::Product::BitBox02BtcOnly => JsValue::from_str("bitbox02-btconly").into(),
            crate::Product::BitBox02NovaMulti => JsValue::from_str("bitbox02-nova-multi").into(),
            crate::Product::BitBox02NovaBtcOnly => {
                JsValue::from_str("bitbox02-nova-btconly").into()
            }
        }
    }

    /// Returns the firmware version, e.g. "9.18.0".
    #[wasm_bindgen(js_name = version)]
    pub fn version(&self) -> String {
        self.device.version().to_string()
    }

    /// Returns the hex-encoded 4-byte root fingerprint.
    #[wasm_bindgen(js_name = rootFingerprint)]
    pub async fn root_fingerprint(&self) -> Result<String, JavascriptError> {
        Ok(self.device.root_fingerprint().await?)
    }

    /// Show recovery words on the Bitbox.
    #[wasm_bindgen(js_name = showMnemonic)]
    pub async fn show_mnemonic(&self) -> Result<(), JavascriptError> {
        Ok(self.device.show_mnemonic().await?)
    }

    /// Invokes the password change workflow on the device.
    #[wasm_bindgen(js_name = changePassword)]
    pub async fn change_password(&self) -> Result<(), JavascriptError> {
        Ok(self.device.change_password().await?)
    }

    /// Retrieves an xpub. For non-standard keypaths, a warning is displayed on the BitBox even if
    /// `display` is false.
    #[wasm_bindgen(js_name = btcXpub)]
    pub async fn btc_xpub(
        &self,
        coin: types::TsBtcCoin,
        keypath: types::TsKeypath,
        xpub_type: types::TsXPubType,
        display: bool,
    ) -> Result<String, JavascriptError> {
        Ok(self
            .device
            .btc_xpub(
                coin.try_into()?,
                &keypath.try_into()?,
                xpub_type.try_into()?,
                display,
            )
            .await?)
    }

    /// Query the device for xpubs. The result contains one xpub per requested keypath.
    #[wasm_bindgen(js_name = btcXpubs)]
    pub async fn btc_xpubs(
        &self,
        coin: types::TsBtcCoin,
        keypaths: Vec<types::TsKeypath>,
        xpub_type: types::TsBtcXPubsType,
    ) -> Result<types::TsBtcXpubs, JavascriptError> {
        let xpubs = self
            .device
            .btc_xpubs(
                coin.try_into()?,
                keypaths
                    .into_iter()
                    .map(|kp| kp.try_into())
                    .collect::<Result<Vec<crate::Keypath>, _>>()?
                    .as_slice(),
                xpub_type.try_into()?,
            )
            .await?;
        Ok(serde_wasm_bindgen::to_value(&xpubs).unwrap().into())
    }

    /// Before a multisig or policy script config can be used to display receive addresses or sign
    /// transactions, it must be registered on the device. This function checks if the script config
    /// was already registered.
    ///
    /// `keypath_account` must be set if the script config is multisig, and can be `undefined` if it
    /// is a policy.
    #[wasm_bindgen(js_name = btcIsScriptConfigRegistered)]
    pub async fn btc_is_script_config_registered(
        &self,
        coin: types::TsBtcCoin,
        script_config: types::TsBtcScriptConfig,
        keypath_account: Option<types::TsKeypath>,
    ) -> Result<bool, JavascriptError> {
        Ok(self
            .device
            .btc_is_script_config_registered(
                coin.try_into()?,
                &script_config.try_into()?,
                keypath_account
                    .map(|kp| kp.try_into())
                    .transpose()?
                    .as_ref(),
            )
            .await?)
    }

    /// Before a multisig or policy script config can be used to display receive addresses or sign
    /// transcations, it must be registered on the device.
    ///
    /// If no name is provided, the user will be asked to enter it on the device instead.  If
    /// provided, it must be non-empty, smaller or equal to 30 chars, consist only of printable
    /// ASCII characters, and contain no whitespace other than spaces.
    ///
    ///
    /// `keypath_account` must be set if the script config is multisig, and can be `undefined` if it
    /// is a policy.
    #[wasm_bindgen(js_name = btcRegisterScriptConfig)]
    pub async fn btc_register_script_config(
        &self,
        coin: types::TsBtcCoin,
        script_config: types::TsBtcScriptConfig,
        keypath_account: Option<types::TsKeypath>,
        xpub_type: types::TsBtcRegisterXPubType,
        name: Option<String>,
    ) -> Result<(), JavascriptError> {
        Ok(self
            .device
            .btc_register_script_config(
                coin.try_into()?,
                &script_config.try_into()?,
                keypath_account
                    .map(|kp| kp.try_into())
                    .transpose()?
                    .as_ref(),
                xpub_type.try_into()?,
                name.as_deref(),
            )
            .await?)
    }

    /// Retrieves a Bitcoin address at the provided keypath.
    ///
    /// For the simple script configs (single-sig), the keypath must follow the
    /// BIP44/BIP49/BIP84/BIP86 conventions.
    #[wasm_bindgen(js_name = btcAddress)]
    pub async fn btc_address(
        &self,
        coin: types::TsBtcCoin,
        keypath: types::TsKeypath,
        script_config: types::TsBtcScriptConfig,
        display: bool,
    ) -> Result<String, JavascriptError> {
        Ok(self
            .device
            .btc_address(
                coin.try_into()?,
                &keypath.try_into()?,
                &script_config.try_into()?,
                display,
            )
            .await?)
    }

    /// Sign a PSBT.
    ///
    /// If `force_script_config` is `undefined`, we attempt to infer the involved script
    /// configs. For the simple script config (single sig), we infer the script config from the
    /// involved redeem scripts and provided derviation paths.
    ///
    /// Multisig and policy configs are currently not inferred and must be provided using
    /// `force_script_config`.
    #[wasm_bindgen(js_name = btcSignPSBT)]
    pub async fn btc_sign_psbt(
        &self,
        coin: types::TsBtcCoin,
        psbt: &str,
        force_script_config: Option<types::TsBtcScriptConfigWithKeypath>,
        format_unit: types::TsBtcFormatUnit,
    ) -> Result<String, JavascriptError> {
        let mut psbt = bitcoin::psbt::Psbt::from_str(psbt.trim())?;
        self.device
            .btc_sign_psbt(
                coin.try_into()?,
                &mut psbt,
                match force_script_config {
                    Some(sc) => Some(sc.try_into()?),
                    None => None,
                },
                format_unit.try_into()?,
            )
            .await?;
        Ok(psbt.to_string())
    }

    #[wasm_bindgen(js_name = btcSignMessage)]
    pub async fn btc_sign_message(
        &self,
        coin: types::TsBtcCoin,
        script_config: types::TsBtcScriptConfigWithKeypath,
        msg: &[u8],
    ) -> Result<types::TsBtcSignMessageSignature, JavascriptError> {
        let signature = self
            .device
            .btc_sign_message(coin.try_into()?, script_config.try_into()?, msg)
            .await?;

        Ok(serde_wasm_bindgen::to_value(&signature).unwrap().into())
    }

    /// Does this device support ETH functionality? Currently this means BitBox02 Multi.
    #[wasm_bindgen(js_name = ethSupported)]
    pub fn eth_supported(&self) -> bool {
        self.device.eth_supported()
    }

    /// Query the device for an xpub.
    #[wasm_bindgen(js_name = ethXpub)]
    pub async fn eth_xpub(&self, keypath: types::TsKeypath) -> Result<String, JavascriptError> {
        Ok(self.device.eth_xpub(&keypath.try_into()?).await?)
    }

    /// Query the device for an Ethereum address.
    #[wasm_bindgen(js_name = ethAddress)]
    pub async fn eth_address(
        &self,
        chain_id: u64,
        keypath: types::TsKeypath,
        display: bool,
    ) -> Result<String, JavascriptError> {
        Ok(self
            .device
            .eth_address(chain_id, &keypath.try_into()?, display)
            .await?)
    }

    /// Signs an Ethereum transaction. It returns a 65 byte signature (R, S, and 1 byte recID).
    #[wasm_bindgen(js_name = ethSignTransaction)]
    pub async fn eth_sign_transaction(
        &self,
        chain_id: u64,
        keypath: types::TsKeypath,
        tx: types::TsEthTransaction,
        address_case: Option<types::TsEthAddressCase>,
    ) -> Result<types::TsEthSignature, JavascriptError> {
        let signature = self
            .device
            .eth_sign_transaction(
                chain_id,
                &keypath.try_into()?,
                &tx.try_into()?,
                address_case.map(TryInto::try_into).transpose()?,
            )
            .await?;

        let v: u64 = compute_v(chain_id, signature[64])
            .ok_or(JavascriptError::ChainIDTooLarge { chain_id })?;
        Ok(serde_wasm_bindgen::to_value(&types::EthSignature {
            r: signature[..32].to_vec(),
            s: signature[32..64].to_vec(),
            v: crate::util::remove_leading_zeroes(&v.to_be_bytes()),
        })
        .unwrap()
        .into())
    }

    /// Signs an Ethereum type 2 transaction according to EIP 1559. It returns a 65 byte signature (R, S, and 1 byte recID).
    #[wasm_bindgen(js_name = ethSign1559Transaction)]
    pub async fn eth_sign_1559_transaction(
        &self,
        keypath: types::TsKeypath,
        tx: types::TsEth1559Transaction,
        address_case: Option<types::TsEthAddressCase>,
    ) -> Result<types::TsEthSignature, JavascriptError> {
        let signature = self
            .device
            .eth_sign_1559_transaction(
                &keypath.try_into()?,
                &tx.try_into()?,
                address_case.map(TryInto::try_into).transpose()?,
            )
            .await?;

        Ok(serde_wasm_bindgen::to_value(&types::EthSignature {
            r: signature[..32].to_vec(),
            s: signature[32..64].to_vec(),
            v: vec![signature[64]],
        })
        .unwrap()
        .into())
    }

    /// Signs an Ethereum message. The provided msg will be prefixed with "\x19Ethereum message\n" +
    /// len(msg) in the hardware, e.g. "\x19Ethereum\n5hello" (yes, the len prefix is the ascii
    /// representation with no fixed size or delimiter).  It returns a 65 byte signature (R, S, and
    /// 1 byte recID). 27 is added to the recID to denote an uncompressed pubkey.
    #[wasm_bindgen(js_name = ethSignMessage)]
    pub async fn eth_sign_message(
        &self,
        chain_id: u64,
        keypath: types::TsKeypath,
        msg: &[u8],
    ) -> Result<types::TsEthSignature, JavascriptError> {
        let signature = self
            .device
            .eth_sign_message(chain_id, &keypath.try_into()?, msg)
            .await?;

        Ok(serde_wasm_bindgen::to_value(&types::EthSignature {
            r: signature[..32].to_vec(),
            s: signature[32..64].to_vec(),
            v: vec![signature[64]], // offset of 27 is already included
        })
        .unwrap()
        .into())
    }

    /// Signs an Ethereum EIP-712 typed message. It returns a 65 byte signature (R, S, and 1 byte
    /// recID). 27 is added to the recID to denote an uncompressed pubkey.
    /// `use_antiklepto` defaults to `true` if omitted.
    #[wasm_bindgen(js_name = ethSignTypedMessage)]
    pub async fn eth_sign_typed_message(
        &self,
        chain_id: u64,
        keypath: types::TsKeypath,
        msg: JsValue,
        use_antiklepto: Option<bool>,
    ) -> Result<types::TsEthSignature, JavascriptError> {
        let json_msg: String = js_sys::JSON::stringify(&msg).unwrap().into();
        let signature = self
            .device
            .eth_sign_typed_message(
                chain_id,
                &keypath.try_into()?,
                &json_msg,
                use_antiklepto.unwrap_or(true),
            )
            .await?;

        Ok(serde_wasm_bindgen::to_value(&types::EthSignature {
            r: signature[..32].to_vec(),
            s: signature[32..64].to_vec(),
            v: vec![signature[64]], // offset of 27 is already included
        })
        .unwrap()
        .into())
    }

    /// Does this device support Cardano functionality? Currently this means BitBox02 Multi.
    #[wasm_bindgen(js_name = cardanoSupported)]
    pub fn cardano_supported(&self) -> bool {
        self.device.cardano_supported()
    }

    /// Query the device for xpubs. The result contains one xpub per requested keypath. Each xpub is
    /// 64 bytes: 32 byte chain code + 32 byte pubkey.
    #[wasm_bindgen(js_name = cardanoXpubs)]
    pub async fn cardano_xpubs(
        &self,
        keypaths: Vec<types::TsKeypath>,
    ) -> Result<types::TsCardanoXpubs, JavascriptError> {
        let xpubs = self
            .device
            .cardano_xpubs(
                keypaths
                    .into_iter()
                    .map(|kp| kp.try_into())
                    .collect::<Result<Vec<crate::Keypath>, _>>()?
                    .as_slice(),
            )
            .await?;
        Ok(serde_wasm_bindgen::to_value(&xpubs).unwrap().into())
    }

    /// Query the device for a Cardano address.
    #[wasm_bindgen(js_name = cardanoAddress)]
    pub async fn cardano_address(
        &self,
        network: types::TsCardanoNetwork,
        script_config: types::TsCardanoScriptConfig,
        display: bool,
    ) -> Result<String, JavascriptError> {
        Ok(self
            .device
            .cardano_address(network.try_into()?, &script_config.try_into()?, display)
            .await?)
    }

    /// Sign a Cardano transaction.
    #[wasm_bindgen(js_name = cardanoSignTransaction)]
    pub async fn cardano_sign_transaction(
        &self,
        transaction: types::TsCardanoTransaction,
    ) -> Result<types::TsCardanoSignTransactionResult, JavascriptError> {
        let tt = transaction.try_into()?;
        let result = self.device.cardano_sign_transaction(tt).await?;
        Ok(serde_wasm_bindgen::to_value(&result).unwrap().into())
    }

    /// Invokes the BIP85-BIP39 workflow on the device, letting the user select the number of words
    /// (12, 28, 24) and an index and display a derived BIP-39 mnemonic.
    #[wasm_bindgen(js_name = bip85AppBip39)]
    pub async fn bip85_app_bip39(&self) -> Result<(), JavascriptError> {
        Ok(self.device.bip85_app_bip39().await?)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_compute_v() {
        // Test with some known values
        assert_eq!(compute_v(1, 0), Some(37));
        assert_eq!(compute_v(1, 1), Some(38));

        // Test with a chain_id that would cause overflow when multiplied by 2
        let large_chain_id = u64::MAX / 2 + 1;
        assert_eq!(compute_v(large_chain_id, 0), None);

        // Test with values that would cause overflow in the final addition
        let chain_id_close_to_overflow = (u64::MAX - 35) / 2;
        assert_eq!(compute_v(chain_id_close_to_overflow, 1), None);
    }
}