Skip to content
Merged

Xpubs #111

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG-npm.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 0.11.0
- Add `btcXpubs()`

## 0.10.1
- package.json: use "main" instead of "module" to fix compatiblity with vitest

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG-rust.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 0.10.0
- Add `btc_xpubs()`

## 0.9.0
- Add support for BitBox02 Nova

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "bitbox-api"
authors = ["Marko Bencun <[email protected]>"]
version = "0.9.0"
version = "0.10.0"
homepage = "https://bitbox.swiss/"
repository = "https://github.com/BitBoxSwiss/bitbox-api-rs/"
readme = "README-rust.md"
Expand Down
2 changes: 1 addition & 1 deletion NPM_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.10.1
0.11.0
2 changes: 1 addition & 1 deletion README-rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Use `--nocapture` to also see some useful simulator output.
If you want to test against a custom simulator build (e.g. when developing new firmware features),
you can run:

SIMULATOR=/path/to/simulator cargo test --features=simulator,tokio
SIMULATOR=/path/to/simulator cargo test --features=simulator,tokio -- --test-threads 1

In this case, only the given simulator will be used, and the ones defined in simulators.json will be
ignored.
10 changes: 10 additions & 0 deletions messages/bitbox02_system.proto
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,23 @@ message DeviceInfoRequest {
}

message DeviceInfoResponse {
message Bluetooth {
// Hash of the currently active Bluetooth firmware on the device.
bytes firmware_hash = 1;
// Firmware version, formated as an unsigned integer "1", "2", etc.
string firmware_version = 2;
// True if Bluetooth is enabled
bool enabled = 3;
}
string name = 1;
bool initialized = 2;
string version = 3;
bool mnemonic_passphrase_enabled = 4;
uint32 monotonic_increments_remaining = 5;
// From v9.6.0: "ATECC608A" or "ATECC608B".
string securechip_model = 6;
// Only present in Bluetooth-enabled devices.
optional Bluetooth bluetooth = 7;
}

message InsertRemoveSDCardRequest {
Expand Down
50 changes: 50 additions & 0 deletions messages/bluetooth.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2025 Shift Crypto AG
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

syntax = "proto3";
package shiftcrypto.bitbox02;

message BluetoothToggleEnabledRequest {
}

message BluetoothUpgradeInitRequest {
uint32 firmware_length = 1;
}

message BluetoothChunkRequest {
bytes data = 1;
}

message BluetoothSuccess {
}

message BluetoothRequestChunkResponse {
uint32 offset = 1;
uint32 length = 2;
}

message BluetoothRequest {
oneof request {
BluetoothUpgradeInitRequest upgrade_init = 1;
BluetoothChunkRequest chunk = 2;
BluetoothToggleEnabledRequest toggle_enabled = 3;
}
}

message BluetoothResponse {
oneof response {
BluetoothSuccess success = 1;
BluetoothRequestChunkResponse request_chunk = 2;
}
}
13 changes: 13 additions & 0 deletions messages/btc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ message BTCPubRequest {
bool display = 5;
}

message BTCXpubsRequest{
enum XPubType {
UNKNOWN = 0;
XPUB = 1;
TPUB = 2;
}
BTCCoin coin = 1;
XPubType xpub_type = 2;
repeated Keypath keypaths = 3;
}

message BTCScriptConfigWithKeypath {
BTCScriptConfig script_config = 2;
repeated uint32 keypath = 3;
Expand Down Expand Up @@ -281,6 +292,7 @@ message BTCRequest {
BTCSignMessageRequest sign_message = 6;
AntiKleptoSignatureRequest antiklepto_signature = 7;
BTCPaymentRequestRequest payment_request = 8;
BTCXpubsRequest xpubs = 9;
}
}

Expand All @@ -291,5 +303,6 @@ message BTCResponse {
BTCSignNextResponse sign_next = 3;
BTCSignMessageResponse sign_message = 4;
AntiKleptoSignerCommitment antiklepto_signer_commitment = 5;
PubsResponse pubs = 6;
}
}
4 changes: 4 additions & 0 deletions messages/common.proto
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ message PubResponse {
string pub = 1;
}

message PubsResponse {
repeated string pubs = 1;
}

message RootFingerprintRequest {
}

Expand Down
3 changes: 3 additions & 0 deletions messages/hww.proto
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import "common.proto";

import "backup_commands.proto";
import "bitbox02_system.proto";
import "bluetooth.proto";
import "btc.proto";
import "cardano.proto";
import "eth.proto";
Expand Down Expand Up @@ -67,6 +68,7 @@ message Request {
ElectrumEncryptionKeyRequest electrum_encryption_key = 26;
CardanoRequest cardano = 27;
BIP85Request bip85 = 28;
BluetoothRequest bluetooth = 29;
}
}

Expand All @@ -89,5 +91,6 @@ message Response {
ElectrumEncryptionKeyResponse electrum_encryption_key = 14;
CardanoResponse cardano = 15;
BIP85Response bip85 = 16;
BluetoothResponse bluetooth = 17;
}
}
70 changes: 70 additions & 0 deletions sandbox/src/Bitcoin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,73 @@ function BtcXPub({ bb02 } : Props) {
);
}


function BtcXPubs({ bb02 } : Props) {
const [coin, setCoin] = useState<bitbox.BtcCoin>('btc');
const [keypaths, setKeypaths] = useState(`["m/49'/0'/0'", "m/84'/0'/0'", "m/86'/0'/0'"]`);
const [xpubType, setXpubType] = useState<bitbox.BtcXPubsType>('xpub');
const [result, setResult] = useState<bitbox.BtcXpubs | undefined>();
const [running, setRunning] = useState(false);
const [err, setErr] = useState<bitbox.Error>();

const btcXPubsTypeOptions = ['tpub', 'xpub'];

const submitForm = async (e: FormEvent) => {
e.preventDefault();
setRunning(true);
setResult(undefined);
setErr(undefined);
try {
setResult(await bb02.btcXpubs(coin, JSON.parse(keypaths), xpubType));
} catch (err) {
throw err;
setErr(bitbox.ensureError(err));
} finally {
setRunning(false);
}
}


return (
<div>
<h4>Multiple XPubs</h4>
<form className="verticalForm"onSubmit={submitForm}>
<label>
Coin
<select value={coin} onChange={e => setCoin(e.target.value as bitbox.BtcCoin)}>
{btcCoinOptions.map(option => <option key={option} value={option}>{option}</option>)}
</select>
</label>
<label>
Keypaths
</label>
<textarea value={keypaths} onChange={e => setKeypaths(e.target.value)} rows={5} cols={80} />
<label>
XPub Type
<select value={xpubType} onChange={e => setXpubType(e.target.value as bitbox.BtcXPubsType)}>
{btcXPubsTypeOptions.map(option => <option key={option} value={option}>{option}</option>)}
</select>
</label>

<button type='submit' disabled={running}>Get XPubs</button>
{result ? <>
<div className="resultContainer">
<label>Result</label>
{
result.map((xpub, i) => (
<code key={i}>
{i}: <b>{xpub}</b><br />
</code>
))
}
</div>
</> : null}
<ShowError err={err} />
</form>
</div>
);
}

function BtcAddressSimple({ bb02 }: Props) {
const [coin, setCoin] = useState<bitbox.BtcCoin>('btc');
const [simpleType, setSimpleType] = useState<bitbox.BtcSimpleType>('p2wpkhP2sh');
Expand Down Expand Up @@ -451,6 +518,9 @@ export function Bitcoin({ bb02 } : Props) {
<div className="action">
<BtcXPub bb02={bb02} />
</div>
<div className="action">
<BtcXPubs bb02={bb02} />
</div>
<div className="action">
<BtcAddressSimple bb02={bb02} />
</div>
Expand Down
37 changes: 37 additions & 0 deletions src/btc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,43 @@ impl<R: Runtime> PairedBitBox<R> {
}
}

/// Retrieves multiple xpubs at once. Only standard keypaths are allowed.
/// On firmware <v9.24.0, this falls back to calling `btc_xpub()` for each keypath.
pub async fn btc_xpubs(
&self,
coin: pb::BtcCoin,
keypaths: &[Keypath],
xpub_type: pb::btc_xpubs_request::XPubType,
) -> Result<Vec<String>, Error> {
if self.validate_version(">=9.24.0").is_err() {
// Fallback to fetching them one-by-one on older firmware.
let mut xpubs = Vec::<String>::with_capacity(keypaths.len());
for keypath in keypaths {
let converted_xpub_type = match xpub_type {
pb::btc_xpubs_request::XPubType::Unknown => return Err(Error::Unknown),
pb::btc_xpubs_request::XPubType::Tpub => pb::btc_pub_request::XPubType::Tpub,
pb::btc_xpubs_request::XPubType::Xpub => pb::btc_pub_request::XPubType::Xpub,
};
let xpub = self
.btc_xpub(coin, keypath, converted_xpub_type, false)
.await?;
xpubs.push(xpub);
}
return Ok(xpubs);
}
match self
.query_proto_btc(pb::btc_request::Request::Xpubs(pb::BtcXpubsRequest {
coin: coin as _,
xpub_type: xpub_type as _,
keypaths: keypaths.iter().map(|kp| kp.into()).collect(),
}))
.await?
{
pb::btc_response::Response::Pubs(pb::PubsResponse { pubs }) => Ok(pubs),
_ => Err(Error::UnexpectedResponse),
}
}

/// Retrieves a Bitcoin address at the provided keypath.
///
/// For the simple script configs (single-sig), the keypath must follow the
Expand Down
Loading