diff --git a/CHANGELOG.md b/CHANGELOG.md index 761087490..d62316c75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ SPDX-License-Identifier: GPL-3.0-or-later --> ## Head +- 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) diff --git a/extmod/foundation-rust/include/foundation.h b/extmod/foundation-rust/include/foundation.h index 584199d16..7ed1c552f 100644 --- a/extmod/foundation-rust/include/foundation.h +++ b/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 * diff --git a/extmod/foundation-rust/src/ur/encoder.rs b/extmod/foundation-rust/src/ur/encoder.rs index 1b8486ed5..4b7dcf7e2 100644 --- a/extmod/foundation-rust/src/ur/encoder.rs +++ b/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 = static mut UR_ENCODER_MESSAGE: heapless::Vec = 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); + } + } +} diff --git a/extmod/foundation/modfoundation-ur.h b/extmod/foundation/modfoundation-ur.h index 9ac5cfb71..050d6b68a 100644 --- a/extmod/foundation/modfoundation-ur.h +++ b/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)}, diff --git a/ports/stm32/boards/Passport/manifest.py b/ports/stm32/boards/Passport/manifest.py index d5f74bea8..7b1cde83a 100644 --- a/ports/stm32/boards/Passport/manifest.py +++ b/ports/stm32/boards/Passport/manifest.py @@ -369,6 +369,7 @@ 'wallets/vault.py', 'wallets/keeper.py', 'wallets/theya.py', + 'wallets/unchained.py', 'wallets/zeus.py')) # Extensions diff --git a/ports/stm32/boards/Passport/modules/tests/test_unit.py b/ports/stm32/boards/Passport/modules/tests/test_unit.py index a088f75fb..526a0d7c9 100644 --- a/ports/stm32/boards/Passport/modules/tests/test_unit.py +++ b/ports/stm32/boards/Passport/modules/tests/test_unit.py @@ -26,3 +26,7 @@ def test_ui(test): def test_foundation(test): assert test('foundation.py') == b'OK' + + +def test_unchained(test): + assert test('unchained.py') == b'OK' diff --git a/ports/stm32/boards/Passport/modules/tests/unit/foundation.py b/ports/stm32/boards/Passport/modules/tests/unit/foundation.py index 9ab69a43f..252c54cd4 100644 --- a/ports/stm32/boards/Passport/modules/tests/unit/foundation.py +++ b/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, diff --git a/ports/stm32/boards/Passport/modules/tests/unit/unchained.py b/ports/stm32/boards/Passport/modules/tests/unit/unchained.py new file mode 100644 index 000000000..447332c7b --- /dev/null +++ b/ports/stm32/boards/Passport/modules/tests/unit/unchained.py @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: © 2026 Foundation Devices, Inc. +# 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') diff --git a/ports/stm32/boards/Passport/modules/wallets/sw_wallets.py b/ports/stm32/boards/Passport/modules/wallets/sw_wallets.py index 9753de4b7..ef5f19013 100644 --- a/ports/stm32/boards/Passport/modules/wallets/sw_wallets.py +++ b/ports/stm32/boards/Passport/modules/wallets/sw_wallets.py @@ -24,6 +24,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. @@ -49,5 +50,6 @@ SparrowWallet, SpecterWallet, TheyaWallet, + UnchainedWallet, ZeusWallet, ] diff --git a/ports/stm32/boards/Passport/modules/wallets/unchained.py b/ports/stm32/boards/Passport/modules/wallets/unchained.py new file mode 100644 index 000000000..9fce70ab1 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/unchained.py @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: © 2026 Foundation Devices, Inc. +# 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'} + ] +} diff --git a/simulator/README.md b/simulator/README.md index c7980f5c0..d9b2ecec8 100644 --- a/simulator/README.md +++ b/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. - - diff --git a/simulator/simulator.py b/simulator/simulator.py index bd217e6b1..ea2df076f 100755 --- a/simulator/simulator.py +++ b/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 C: copy-selection(CLIPBOARD)\n' + 'Ctrl Shift 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))