Merge pull request #669 from Foundation-Devices/add-native-unchained-connect-wallet
What changed, and why it matters
This commit adds support for the Unchained wallet to the Passport hardware wallet. Most of the change is normal feature code, but it also introduces a new way to feed already-encoded data into the QR encoder and tightens up the encoder so it won't produce output if it hasn't been started correctly. The simulator also gets clipboard copy/paste tweaks. There is no vendor statement that this fixes a security bug, and the changes look like defensive hardening rather than a patch for an active vulnerability.
Review the new `ur_encoder_start_raw` path for memory-safety invariants, ensure callers cannot pass attacker-controlled UR types or CBOR lengths, and confirm the `started` flag is reset consistently on all failure paths. Treat as routine feature code with minor defensive hardening; no urgent security response indicated.
Security signals we found
New unsafe FFI function `ur_encoder_start_raw` added with documented safety preconditions
UR type string is validated (length, charset, UTF-8) before use in encoder
Encoder now tracks a `started` flag and returns empty output if not started, reducing use-after-free/misuse risk
No vendor disclosure of security relevance, CVE, or researcher attribution in commit or references
Evidence from the diff
The merge adds Unchained as a multisig ‘Connect Wallet’ option. Core technical changes: (1) a new RawValue UR type and ur_encoder_start_raw C/Rust API that accepts pre-CBOR-encoded UR payloads, with validation of the UR type string (lowercase/ digits/hyphen, non-empty, length bound); (2) an encoder started flag so ur_encoder_next_part returns an empty string instead of undefined behavior if the encoder was not initialized; (3) new MicroPython bindings and unit tests; (4) simulator xterm clipboard keybindings. The encoder hardening reduces a misuse footgun but the commit itself is framed as a feature addition.
Changed components
extmod/foundation-rust/src/ur/encoder.rsextmod/foundation/modfoundation-ur.hports/stm32/boards/Passport/modules/wallets/unchained.pyports/stm32/boards/Passport/modules/wallets/sw_wallets.pysimulator/simulator.pyInspect captured patch +426 / −18
### CHANGELOG.md
@@ -10,6 +10,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
- Validate the complete local xpub when importing multisig wallets
- Require confirmation before using PSBT-proposed multisig wallets with temporary seeds,
and cancel signing if import is declined
+- Added Unchained as a multisig Connect Wallet option
- Added Coconut Wallet as a single-sig Connect Wallet option
- Improved self-send transaction information formatting (PASS1-638)
- Added the key manager extension, compatible with BIP85 and Nostr (PASS1-24)
### extmod/foundation-rust/include/foundation.h
@@ -614,13 +614,29 @@ void ur_encoder_start(UR_Encoder *encoder,
const UR_Value *value,
size_t max_chars);
+/**
+ * Start the encoder with an already CBOR-encoded Uniform Resource.
+ *
+ * # Safety
+ *
+ * `ur_type` and `message` must be valid for reads of their respective
+ * lengths for the duration of this call. The caller is responsible for
+ * ensuring that `message` contains well-formed CBOR.
+ */
+bool ur_encoder_start_raw(UR_Encoder *encoder,
+ const uint8_t *ur_type,
+ size_t ur_type_len,
+ const uint8_t *message,
+ size_t message_len,
+ size_t max_chars);
+
/**
* Returns the UR corresponding to the next fountain encoded part.
*
* # Safety
*
- * This function must not be called if `ur_encoder_start` was not called to
- * start the encoder. Or if the data used to start the encoder is freed.
+ * `ur` and `ur_len` must be valid for writes. If the encoder has not been
+ * started successfully, this function returns an empty string.
*
* # Return Value
*
### extmod/foundation-rust/src/ur/encoder.rs
@@ -3,7 +3,7 @@
//! Encoder.
-use core::{ffi::c_char, fmt::Write, ptr};
+use core::{ffi::c_char, fmt::Write, ptr, slice, str};
use foundation_ur::{max_fragment_len, HeaplessEncoder};
use minicbor::{Encode, Encoder};
@@ -55,6 +55,7 @@ pub const UR_ENCODER_MAX_MESSAGE_LEN: usize = UR_DECODER_MAX_MESSAGE_LEN;
#[cfg_attr(dtcm, link_section = ".dtcm")]
pub static mut UR_ENCODER: UR_Encoder = UR_Encoder {
inner: HeaplessEncoder::new(),
+ started: false,
};
/// cbindgen:ignore
@@ -69,6 +70,12 @@ static mut UR_ENCODER_STRING: heapless::Vec<u8, UR_ENCODER_MAX_STRING> =
static mut UR_ENCODER_MESSAGE: heapless::Vec<u8, UR_ENCODER_MAX_MESSAGE_LEN> =
heapless::Vec::new();
+/// cbindgen:ignore
+#[used]
+#[cfg_attr(sram4, link_section = ".sram4")]
+static mut UR_ENCODER_TYPE: heapless::String<{ UR_MAX_TYPE.len() }> =
+ heapless::String::new();
+
/// Uniform Resource encoder.
pub struct UR_Encoder {
inner: HeaplessEncoder<
@@ -77,6 +84,7 @@ pub struct UR_Encoder {
UR_ENCODER_MAX_FRAGMENT_LEN,
UR_ENCODER_MAX_SEQUENCE_COUNT,
>,
+ started: bool,
}
/// Start the encoder.
@@ -117,14 +125,71 @@ pub unsafe extern "C" fn ur_encoder_start(
message,
max_fragment_len(UR_MAX_TYPE, usize::MAX, max_chars),
);
+ encoder.started = true;
+}
+
+/// Start the encoder with an already CBOR-encoded Uniform Resource.
+///
+/// # Safety
+///
+/// `ur_type` and `message` must be valid for reads of their respective
+/// lengths for the duration of this call. The caller is responsible for
+/// ensuring that `message` contains well-formed CBOR.
+#[no_mangle]
+pub unsafe extern "C" fn ur_encoder_start_raw(
+ encoder: &mut UR_Encoder,
+ ur_type: *const u8,
+ ur_type_len: usize,
+ message: *const u8,
+ message_len: usize,
+ max_chars: usize,
+) -> bool {
+ // A rejected value must not leave the previously encoded UR available.
+ encoder.started = false;
+
+ let ur_type = unsafe { slice::from_raw_parts(ur_type, ur_type_len) };
+ let message = unsafe { slice::from_raw_parts(message, message_len) };
+
+ let Ok(ur_type) = str::from_utf8(ur_type) else {
+ return false;
+ };
+ if ur_type.is_empty()
+ || ur_type.len() > UR_MAX_TYPE.len()
+ || !ur_type
+ .bytes()
+ .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'-')
+ {
+ return false;
+ }
+
+ let encoder_type = unsafe { &mut *ptr::addr_of_mut!(UR_ENCODER_TYPE) };
+ encoder_type.clear();
+ if encoder_type.push_str(ur_type).is_err() {
+ return false;
+ }
+
+ let encoder_message =
+ unsafe { &mut *ptr::addr_of_mut!(UR_ENCODER_MESSAGE) };
+ encoder_message.clear();
+ if encoder_message.extend_from_slice(message).is_err() {
+ return false;
+ }
+
+ encoder.inner.start(
+ encoder_type.as_str(),
+ encoder_message,
+ max_fragment_len(UR_MAX_TYPE, usize::MAX, max_chars),
+ );
+ encoder.started = true;
+ true
}
/// Returns the UR corresponding to the next fountain encoded part.
///
/// # Safety
///
-/// This function must not be called if `ur_encoder_start` was not called to
-/// start the encoder. Or if the data used to start the encoder is freed.
+/// `ur` and `ur_len` must be valid for writes. If the encoder has not been
+/// started successfully, this function returns an empty string.
///
/// # Return Value
///
@@ -140,6 +205,13 @@ pub unsafe extern "C" fn ur_encoder_next_part(
ur: *mut *const c_char,
ur_len: *mut usize,
) {
+ if !encoder.started {
+ static EMPTY: &[u8] = b"\0";
+ *ur = EMPTY.as_ptr() as *const c_char;
+ *ur_len = 0;
+ return;
+ }
+
let part = encoder.inner.next_part();
let buf = unsafe { &mut *ptr::addr_of_mut!(UR_ENCODER_STRING) };
@@ -163,3 +235,57 @@ impl<'a, const N: usize> minicbor::encode::Write for Writer<'a, N> {
#[derive(Debug)]
struct EndOfSlice;
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use foundation_ur::{HeaplessDecoder, UR};
+
+ #[test]
+ fn raw_encoder_preserves_the_ur_type() {
+ // Encoder FFI storage is process-global, so keep its lifecycle in one test.
+ let ur_type = b"crypto-hdkey";
+ let message = b"\xa0";
+
+ unsafe {
+ let encoder = &mut *ptr::addr_of_mut!(UR_ENCODER);
+ assert!(ur_encoder_start_raw(
+ encoder,
+ ur_type.as_ptr(),
+ ur_type.len(),
+ message.as_ptr(),
+ message.len(),
+ UR_ENCODER_MAX_STRING,
+ ));
+
+ let mut encoded = ptr::null();
+ let mut encoded_len = 0;
+ ur_encoder_next_part(encoder, &mut encoded, &mut encoded_len);
+ let encoded =
+ slice::from_raw_parts(encoded as *const u8, encoded_len);
+ assert!(encoded.starts_with(b"ur:crypto-hdkey/"));
+
+ let encoded = str::from_utf8(encoded).unwrap();
+ let ur = UR::parse(encoded).unwrap();
+ let mut decoder: HeaplessDecoder<16, 2, 32, 8, 8, 16> =
+ HeaplessDecoder::new();
+ decoder.receive(ur).unwrap();
+ assert!(decoder.is_complete());
+ assert_eq!(decoder.message().unwrap().unwrap(), message);
+
+ assert!(!ur_encoder_start_raw(
+ encoder,
+ b"CRYPTO-HDKEY".as_ptr(),
+ b"CRYPTO-HDKEY".len(),
+ message.as_ptr(),
+ message.len(),
+ UR_ENCODER_MAX_STRING,
+ ));
+
+ let mut encoded = ptr::null();
+ let mut encoded_len = usize::MAX;
+ ur_encoder_next_part(encoder, &mut encoded, &mut encoded_len);
+ assert_eq!(encoded_len, 0);
+ }
+ }
+}
### extmod/foundation/modfoundation-ur.h
@@ -10,6 +10,7 @@
/// package: foundation.ur
STATIC const mp_obj_type_t mod_foundation_ur_Value_type;
+STATIC const mp_obj_type_t mod_foundation_ur_RawValue_type;
STATIC const mp_obj_type_t mod_foundation_ur_CoinType_type;
STATIC const mp_obj_type_t mod_foundation_ur_CoinInfo_type;
STATIC const mp_obj_type_t mod_foundation_ur_Keypath_type;
@@ -178,6 +179,30 @@ STATIC const mp_obj_type_t mod_foundation_ur_Value_type = {
.locals_dict = (mp_obj_dict_t *)&mod_foundation_ur_Value_locals_dict,
};
+/// class RawValue:
+/// """
+/// A Uniform Resource whose payload is already CBOR encoded.
+/// """
+typedef struct _mp_obj_RawValue_t {
+ mp_obj_base_t base;
+ mp_obj_t ur_type;
+ mp_obj_t cbor;
+} mp_obj_RawValue_t;
+
+STATIC void mod_foundation_ur_RawValue_print(const mp_print_t *print,
+ mp_obj_t o_in,
+ mp_print_kind_t kind) {
+ (void)o_in;
+ (void)kind;
+ mp_print_str(print, "UR_RawValue");
+}
+
+STATIC const mp_obj_type_t mod_foundation_ur_RawValue_type = {
+ { &mp_type_type },
+ .name = MP_QSTR_RawValue,
+ .print = mod_foundation_ur_RawValue_print,
+};
+
/// class CoinType:
/// """
/// """
@@ -347,6 +372,30 @@ STATIC mp_obj_t mod_foundation_ur_new_bytes(mp_obj_t data_in)
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_foundation_ur_new_bytes_obj,
mod_foundation_ur_new_bytes);
+/// def new_raw(ur_type: str, cbor: bytes) -> RawValue:
+/// """
+/// Create a Uniform Resource from an already CBOR-encoded payload.
+/// """
+STATIC mp_obj_t mod_foundation_ur_new_raw(mp_obj_t ur_type_in,
+ mp_obj_t cbor_in)
+{
+ if (!mp_obj_is_str(ur_type_in)) {
+ mp_raise_msg(&mp_type_ValueError,
+ MP_ERROR_TEXT("ur_type should be a string"));
+ }
+
+ mp_buffer_info_t cbor = {0};
+ mp_get_buffer_raise(cbor_in, &cbor, MP_BUFFER_READ);
+
+ mp_obj_RawValue_t *o = m_new_obj(mp_obj_RawValue_t);
+ o->base.type = &mod_foundation_ur_RawValue_type;
+ o->ur_type = ur_type_in;
+ o->cbor = cbor_in;
+ return MP_OBJ_FROM_PTR(o);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_foundation_ur_new_raw_obj,
+ mod_foundation_ur_new_raw);
+
/// def new_derived_key(key_data=None,
/// is_private=False,
/// chain_code=None,
@@ -519,15 +568,30 @@ STATIC mp_obj_t mod_foundation_ur_encoder_start(mp_obj_t value_in,
mp_obj_Value_t *value = NULL;
mp_int_t max_fragment_len = 0;
- if (!mp_obj_is_type(value_in, &mod_foundation_ur_Value_type)) {
+ max_fragment_len = mp_obj_int_get_uint_checked(max_fragment_len_in);
+
+ if (mp_obj_is_type(value_in, &mod_foundation_ur_Value_type)) {
+ value = MP_OBJ_TO_PTR(value_in);
+ ur_encoder_start(&UR_ENCODER, &value->value, max_fragment_len);
+ } else if (mp_obj_is_type(value_in, &mod_foundation_ur_RawValue_type)) {
+ mp_obj_RawValue_t *raw = MP_OBJ_TO_PTR(value_in);
+ mp_buffer_info_t cbor = {0};
+ GET_STR_DATA_LEN(raw->ur_type, ur_type, ur_type_len);
+ mp_get_buffer_raise(raw->cbor, &cbor, MP_BUFFER_READ);
+
+ if (!ur_encoder_start_raw(&UR_ENCODER,
+ ur_type,
+ ur_type_len,
+ cbor.buf,
+ cbor.len,
+ max_fragment_len)) {
+ mp_raise_msg(&mp_type_ValueError,
+ MP_ERROR_TEXT("invalid raw Uniform Resource"));
+ }
+ } else {
mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("invalid type for value"));
- return mp_const_none;
}
- value = MP_OBJ_TO_PTR(value_in);
- max_fragment_len = mp_obj_int_get_uint_checked(max_fragment_len_in);
- ur_encoder_start(&UR_ENCODER, &value->value, max_fragment_len);
-
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_foundation_ur_encoder_start_obj,
@@ -683,6 +747,7 @@ STATIC const mp_rom_map_elem_t mod_foundation_ur_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR_Keypath), MP_ROM_PTR(&mod_foundation_ur_Keypath_type)},
{MP_ROM_QSTR(MP_QSTR_PassportRequest), MP_ROM_PTR(&mod_foundation_ur_PassportRequest_type)},
{MP_ROM_QSTR(MP_QSTR_new_bytes), MP_ROM_PTR(&mod_foundation_ur_new_bytes_obj)},
+ {MP_ROM_QSTR(MP_QSTR_new_raw), MP_ROM_PTR(&mod_foundation_ur_new_raw_obj)},
{MP_ROM_QSTR(MP_QSTR_new_derived_key), MP_ROM_PTR(&mod_foundation_ur_new_derived_key_obj)},
{MP_ROM_QSTR(MP_QSTR_new_psbt), MP_ROM_PTR(&mod_foundation_ur_new_psbt_obj)},
{MP_ROM_QSTR(MP_QSTR_new_passport_response), MP_ROM_PTR(&mod_foundation_ur_new_passport_response_obj)},
### ports/stm32/boards/Passport/manifest.py
@@ -364,6 +364,7 @@
'wallets/vault.py',
'wallets/keeper.py',
'wallets/theya.py',
+ 'wallets/unchained.py',
'wallets/zeus.py'))
# Extensions
### ports/stm32/boards/Passport/modules/tests/test_unit.py
@@ -50,3 +50,7 @@ def test_multisig_xpub_validation(test):
def test_psbt_fee(test):
assert test('psbt_fee.py') == b'OK'
+
+
+def test_unchained(test):
+ assert test('unchained.py') == b'OK'
### ports/stm32/boards/Passport/modules/tests/unit/foundation.py
@@ -39,6 +39,13 @@ def should_fail(f):
foundation.sha256('this is the message', digest)
assert digest == bytearray(b'\x131qZ\x1f\xfd\x04\xe6`\x04\x93\x1a\x8d\xbc6U\xebJR>\xd5\xece\xecm\x1c\xed\x93x+\xd3\xbd') # nopep8
+raw_ur = foundation.ur.new_raw('crypto-hdkey', b'\xa0')
+foundation.ur.encoder_start(raw_ur, 535)
+assert foundation.ur.encoder_next_part().startswith('ur:crypto-hdkey/')
+
+bad_raw_ur = foundation.ur.new_raw('CRYPTO-HDKEY', b'\xa0')
+should_fail(lambda: foundation.ur.encoder_start(bad_raw_ur, 535))
+
SAMPLE_HOR_RES = 10
SAMPLE_VER_RES = 10
SAMPLE_IMG = [0x04, 0x28, 0x40, 0x01, 0xfb, 0x05, 0xfa, 0x05, 0xfb, 0x05, 0xfa, 0x05,
### ports/stm32/boards/Passport/modules/tests/unit/unchained.py
@@ -0,0 +1,63 @@
+# SPDX-FileCopyrightText: © 2026 Foundation Devices, Inc. <hello@foundation.xyz>
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from ubinascii import unhexlify
+
+from data_codecs.multisig_config_sampler import MultisigConfigSampler
+from data_codecs.qr_type import QRType
+from wallets.multisig_import import read_multisig_config_from_microsd, read_multisig_config_from_qr
+from wallets.sw_wallets import supported_software_wallets
+from wallets.unchained import UnchainedWallet, create_unchained_hdkey_cbor
+
+
+public_key = unhexlify('02' + '11' * 32)
+chain_code = unhexlify('22' * 32)
+fingerprint = 0xf23f9fd2
+
+cbor = create_unchained_hdkey_cbor(public_key,
+ chain_code,
+ fingerprint,
+ fingerprint,
+ False)
+expected = unhexlify(
+ 'a5'
+ '035821' + '02' + '11' * 32 +
+ '045820' + '22' * 32 +
+ '05d90131a0'
+ '06d90130a3'
+ '0182182df5'
+ '021af23f9fd2'
+ '0301'
+ '081af23f9fd2')
+# This pins the required field order and canonical CBOR encodings.
+assert cbor == expected
+
+testnet_cbor = create_unchained_hdkey_cbor(public_key, chain_code, 0, 0, True)
+assert testnet_cbor[0] == 0xa4
+assert b'\x05\xd9\x01\x31\xa1\x02\x01' in testnet_cbor
+assert b'\x06\xd9\x01\x30\xa2\x01\x82\x18\x2d\xf5\x03\x01' in testnet_cbor
+
+assert UnchainedWallet in supported_software_wallets
+assert UnchainedWallet['label'] == 'Unchained'
+assert len(UnchainedWallet['sig_types']) == 1
+
+sig_type = UnchainedWallet['sig_types'][0]
+assert sig_type['id'] == 'multisig'
+assert sig_type['import_qr'] is read_multisig_config_from_qr
+assert sig_type['import_microsd'] is read_multisig_config_from_microsd
+
+export_modes = UnchainedWallet['export_modes']
+assert export_modes[0]['id'] == 'qr'
+assert export_modes[0]['qr_type'] == QRType.UR2
+assert export_modes[1]['id'] == 'microsd'
+assert export_modes[1]['filename_pattern_multisig'] == '{xfp}-unchained-multisig.json'
+
+unchained_config = b'''# Coldcard Multisig setup file (exported from @caravan/wallets)
+Name: example-vault
+Policy: 2 of 3
+Derivation: m/48'/0'/0'/2'
+Format: P2WSH
+'''
+assert MultisigConfigSampler.sample(unchained_config)
+
+return_value.write(b'OK')
### ports/stm32/boards/Passport/modules/wallets/sw_wallets.py
@@ -25,6 +25,7 @@
from .sparrow import SparrowWallet
from .specter import SpecterWallet
from .theya import TheyaWallet
+from .unchained import UnchainedWallet
from .zeus import ZeusWallet
# Array of all supported software wallets and their attributes.
@@ -51,5 +52,6 @@
SparrowWallet,
SpecterWallet,
TheyaWallet,
+ UnchainedWallet,
ZeusWallet,
]
### ports/stm32/boards/Passport/modules/wallets/unchained.py
@@ -0,0 +1,120 @@
+# SPDX-FileCopyrightText: © 2026 Foundation Devices, Inc. <hello@foundation.xyz>
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# unchained.py - Unchained wallet support
+
+import chains
+import stash
+
+from common import settings
+from data_codecs.qr_type import QRType
+from foundation import ur
+from public_constants import AF_P2SH
+from utils import swab32
+
+from .multisig_import import read_multisig_config_from_microsd, read_multisig_config_from_qr
+from .multisig_json import create_multisig_json_wallet
+
+
+def _append_cbor_uint(result, value):
+ assert 0 <= value <= 0xffffffff
+
+ if value < 24:
+ result.append(value)
+ elif value <= 0xff:
+ result.extend(bytes([0x18, value]))
+ elif value <= 0xffff:
+ result.extend(bytes([0x19, value >> 8, value & 0xff]))
+ else:
+ result.extend(bytes([0x1a,
+ (value >> 24) & 0xff,
+ (value >> 16) & 0xff,
+ (value >> 8) & 0xff,
+ value & 0xff]))
+
+
+def create_unchained_hdkey_cbor(public_key,
+ chain_code,
+ source_fingerprint,
+ parent_fingerprint,
+ is_testnet):
+ """Encode the BIP45 xpub in the crypto-hdkey form accepted by Unchained."""
+ assert len(public_key) == 33
+ assert len(chain_code) == 32
+
+ result = bytearray()
+ # Keys 3, 4, 5, and 6 are required; key 8 is optional.
+ result.append(0xa4 + int(parent_fingerprint != 0))
+ result.extend(b'\x03\x58\x21')
+ result.extend(public_key)
+ result.extend(b'\x04\x58\x20')
+ result.extend(chain_code)
+
+ # Unchained's decoder currently requires the original BCR-2020 tags.
+ result.extend(b'\x05\xd9\x01\x31') # crypto-coin-info, tag 305
+ result.extend(b'\xa1\x02\x01' if is_testnet else b'\xa0')
+
+ result.extend(b'\x06\xd9\x01\x30') # crypto-keypath, tag 304
+ # Keys 1 and 3 are required; key 2 is optional.
+ result.append(0xa2 + int(source_fingerprint != 0))
+ result.extend(b'\x01\x82\x18\x2d\xf5') # m/45'
+ if source_fingerprint:
+ result.append(0x02)
+ _append_cbor_uint(result, source_fingerprint)
+ result.extend(b'\x03\x01') # depth 1
+
+ if parent_fingerprint:
+ result.append(0x08)
+ _append_cbor_uint(result, parent_fingerprint)
+
+ return bytes(result)
+
+
+def create_unchained_export(sw_wallet=None,
+ addr_type=None,
+ acct_num=0,
+ multisig=False,
+ legacy=False,
+ export_mode='qr',
+ qr_type=QRType.UR2):
+ assert multisig
+
+ # Unchained's QR registration is BIP45-only. The Sparrow-style microSD
+ # export includes BIP45 plus nested and native BIP48 keys.
+ if export_mode != 'qr':
+ return create_multisig_json_wallet(sw_wallet=sw_wallet,
+ addr_type=addr_type,
+ acct_num=acct_num,
+ multisig=multisig,
+ legacy=legacy,
+ export_mode=export_mode,
+ qr_type=qr_type)
+
+ chain = chains.current_chain()
+ with stash.SensitiveValues() as sv:
+ node = sv.derive_path("m/45'")
+ source_fingerprint = swab32(settings.get('xfp'))
+ cbor = create_unchained_hdkey_cbor(node.public_key(),
+ node.chain_code(),
+ source_fingerprint,
+ node.fingerprint(),
+ chain.ctype != 'BTC')
+
+ return (ur.new_raw('crypto-hdkey', cbor),
+ [{'fmt': AF_P2SH, 'deriv': "m/45'", 'acct': acct_num}])
+
+
+UnchainedWallet = {
+ 'label': 'Unchained',
+ 'sig_types': [
+ {'id': 'multisig', 'label': 'Multisig', 'addr_type': None,
+ 'create_wallet': create_unchained_export,
+ 'import_qr': read_multisig_config_from_qr,
+ 'import_microsd': read_multisig_config_from_microsd}
+ ],
+ 'export_modes': [
+ {'id': 'qr', 'label': 'QR Code', 'qr_type': QRType.UR2},
+ {'id': 'microsd', 'label': 'microSD',
+ 'filename_pattern_multisig': '{xfp}-unchained-multisig.json'}
+ ]
+}
### simulator/README.md
@@ -37,5 +37,3 @@ setenv PKG_CONFIG_PATH /usr/local/opt/libffi/lib/pkgconfig
- Sorry we haven't gotten around to that yet, but certainly would be possible to build
this on Linux or FreeBSD... but not Windows.
-
-
### simulator/simulator.py
@@ -459,11 +459,16 @@ def sock_cleanup():
+ sys.argv[1:]
print('cc_cmd: {}'.format(passport_cmd))
- xterm = subprocess.Popen(['xterm', '-title', 'Passport Simulator REPL',
- '-geom', '132x72+0+0', '-e'] + passport_cmd,
- env=env,
- stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
- pass_fds=pass_fds, shell=False)
+ xterm = subprocess.Popen([
+ 'xterm', '-title', 'Passport Simulator REPL', '-geom', '132x72+0+0',
+ '-xrm', 'XTerm*selectToClipboard: true',
+ '-xrm', 'XTerm*VT100*translations: #override\n'
+ 'Ctrl Shift <Key>C: copy-selection(CLIPBOARD)\n'
+ 'Ctrl Shift <Key>V: insert-selection(CLIPBOARD)',
+ '-e'] + passport_cmd,
+ env=env,
+ stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
+ pass_fds=pass_fds, shell=False)
print("COMMAND: " + " ".join(passport_cmd))
Why this scored 25/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.