diff --git a/cw_bitcoin/lib/hardware/coldcard_service.dart b/cw_bitcoin/lib/hardware/coldcard_service.dart new file mode 100644 index 0000000000..655ff2220a --- /dev/null +++ b/cw_bitcoin/lib/hardware/coldcard_service.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:coldcard_protocol/client.dart'; +import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart'; +import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; +import 'package:cw_core/hardware/hardware_wallet_service.dart'; + +class ColdcardService extends HardwareWalletService with BitcoinHardwareWalletService { + ColdcardService(this.device); + + final ColdCardDevice device; + + @override + Future getMasterFingerprint() async { + return Uint8List(4)..buffer.asByteData().setUint32(0, device.masterFingerprint!, Endian.little); + } + + @override + Future> getAvailableAccounts({int index = 0, int limit = 5}) async { + final accounts = []; + final indexRange = List.generate(limit, (i) => i + index); + + for (final i in indexRange) { + final derivationPath = "m/84'/0'/$i'"; + final xPub = await device.getXpub(derivationPath); + final hd = Bip32Slip10Secp256k1.fromExtendedKey(xPub).childKey(Bip32KeyIndex(0)); + final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet); + + accounts.add(HardwareAccountData( + address: address, + accountIndex: i, + derivationPath: derivationPath, + xpub: xPub, + )); + } + + return accounts; + } + + @override + Future signTransaction({required String transaction}) async => + device.signTransaction(base64Decode(transaction), finalize: true); + + @override + Future signMessage({required Uint8List message, String? derivationPath}) async { + final result = await device.signMessage(message, subpath: derivationPath ?? 'm'); + return result.$2; + } +} diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index b0752cf797..3f54ae831d 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -20,6 +20,14 @@ dependencies: shared_preferences: ^2.0.15 cw_core: path: ../cw_core + coldcard_protocol: + git: + url: https://github.com/warp-records/coldcard-protocol + ref: 30b7ea705fc4cf1ab0aa5c51c3483fa2b60fe4f9 + coldcard_usb: + git: + url: https://github.com/warp-records/coldcard-usb + ref: 16b1115ab23dc21585f7f97ba06375c28fbe2ee7 bitbox: git: url: https://github.com/cake-tech/bitbox-flutter.git diff --git a/cw_core/lib/hardware/device_connection_type.dart b/cw_core/lib/hardware/device_connection_type.dart index 3b1dd93490..a75c545927 100644 --- a/cw_core/lib/hardware/device_connection_type.dart +++ b/cw_core/lib/hardware/device_connection_type.dart @@ -9,6 +9,8 @@ enum DeviceConnectionType { WalletType walletType, HardwareWalletType hardwareType, [bool isIOS = false]) { bool isSupported = false; + bool bleSupport = true; + switch (hardwareType) { case HardwareWalletType.bitbox: isSupported = [ @@ -36,6 +38,10 @@ enum DeviceConnectionType { WalletType.polygon, ].contains(walletType); break; + case HardwareWalletType.coldcardUsb: + isSupported = walletType == WalletType.bitcoin; + bleSupport = false; + break; case HardwareWalletType.cupcake: case HardwareWalletType.coldcard: case HardwareWalletType.seedsigner: @@ -44,11 +50,12 @@ enum DeviceConnectionType { throw UnimplementedError(); } - return isSupported - ? (isIOS - ? [DeviceConnectionType.ble] - : [DeviceConnectionType.ble, DeviceConnectionType.usb]) - : []; + if (!isSupported) return []; + + return [ + if (bleSupport) DeviceConnectionType.ble, + if (!isIOS) DeviceConnectionType.usb, + ]; } String get iconString { diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 5877ad2dab..4e6b79c51f 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -44,7 +44,8 @@ enum HardwareWalletType { coldcard, seedsigner, keystone, - trezor; + trezor, + coldcardUsb; } enum WalletInfoAddressType { @@ -557,7 +558,8 @@ class WalletInfo { bool get isHardwareWallet => [ HardwareWalletType.bitbox, HardwareWalletType.ledger, - HardwareWalletType.trezor + HardwareWalletType.trezor, + HardwareWalletType.coldcardUsb, ].contains(hardwareWalletType); DateTime get date => DateTime.fromMillisecondsSinceEpoch(timestamp); diff --git a/cw_core/lib/wallet_info_legacy.part.dart b/cw_core/lib/wallet_info_legacy.part.dart index d58826e098..9380bcab2d 100644 --- a/cw_core/lib/wallet_info_legacy.part.dart +++ b/cw_core/lib/wallet_info_legacy.part.dart @@ -243,6 +243,8 @@ class HardwareWalletTypeAdapter extends TypeAdapter { return newWi.HardwareWalletType.keystone; case 6: return newWi.HardwareWalletType.trezor; + case 7: + return newWi.HardwareWalletType.coldcardUsb; default: return newWi.HardwareWalletType.ledger; } @@ -272,6 +274,9 @@ class HardwareWalletTypeAdapter extends TypeAdapter { case newWi.HardwareWalletType.trezor: writer.writeByte(6); break; + case newWi.HardwareWalletType.coldcardUsb: + writer.writeByte(7); + break; } } diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 66ec7577c7..d80e71aa05 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -515,6 +515,7 @@ abstract class EVMChainWalletBase break; case HardwareWalletType.cupcake: case HardwareWalletType.coldcard: + case HardwareWalletType.coldcardUsb: case HardwareWalletType.seedsigner: case HardwareWalletType.keystone: throw UnimplementedError(); diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 6f5b9bfa49..8024e6019f 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -564,6 +564,11 @@ class CWBitcoin extends Bitcoin { return LitecoinTrezorService(connect); } + @override + HardwareWalletService getColdcardHardwareWalletService(ColdCardDevice device) { + return ColdcardService(device); + } + @override List getSilentPaymentAddresses(Object wallet) { final bitcoinWallet = wallet as ElectrumWallet; diff --git a/lib/di.dart b/lib/di.dart index 821f51130a..8d2a076290 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -241,6 +241,7 @@ import 'package:cake_wallet/view_model/hardware_wallet/bitbox_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/coldcard_view_model.dart'; import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; @@ -415,6 +416,7 @@ Future setup({ case HardwareWalletType.bitbox: return getIt(); case HardwareWalletType.ledger: return getIt(); case HardwareWalletType.trezor: return getIt(); + case HardwareWalletType.coldcardUsb: return getIt(); case HardwareWalletType.cupcake: case HardwareWalletType.coldcard: case HardwareWalletType.seedsigner: @@ -426,6 +428,8 @@ Future setup({ getIt.registerLazySingleton(() => LedgerViewModel()); getIt.registerLazySingleton(() => BitboxViewModel()); + + getIt.registerLazySingleton(() => ColdcardViewModel()); getIt.registerLazySingleton(() => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet")); diff --git a/lib/entities/hardware_wallet/hardware_wallet_device.dart b/lib/entities/hardware_wallet/hardware_wallet_device.dart index 757e680994..f80a46f228 100644 --- a/lib/entities/hardware_wallet/hardware_wallet_device.dart +++ b/lib/entities/hardware_wallet/hardware_wallet_device.dart @@ -57,6 +57,12 @@ class TrezorHardwareWalletDevice extends HardwareWalletDevice { HardwareWalletConnectionType get connectionType => device.connectionType.toGeneric(); } +class ColdcardHardwareWalletDevice extends HardwareWalletDevice { + @override String get name => 'Coldcard'; + @override HardwareWalletDeviceType get type => HardwareWalletDeviceType.coldcard; + @override HardwareWalletConnectionType get connectionType => HardwareWalletConnectionType.usb; +} + enum HardwareWalletDeviceType { ledgerBlue, ledgerNanoS, @@ -71,7 +77,8 @@ enum HardwareWalletDeviceType { trezorModelT, trezorSafe3, trezorSafe5, - trezorSafe7; + trezorSafe7, + coldcard; } enum HardwareWalletConnectionType { diff --git a/lib/new-ui/widgets/coins_page/wallet_info.dart b/lib/new-ui/widgets/coins_page/wallet_info.dart index 4963513fc9..4993d53261 100644 --- a/lib/new-ui/widgets/coins_page/wallet_info.dart +++ b/lib/new-ui/widgets/coins_page/wallet_info.dart @@ -93,6 +93,8 @@ class WalletInfoBar extends StatelessWidget { return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg"; case HardwareWalletType.cupcake: return "assets/images/cupcake.svg"; + case HardwareWalletType.coldcardUsb: + return "assets/images/hardware_wallet/coldcard_man.svg"; case HardwareWalletType.coldcard: case HardwareWalletType.seedsigner: case HardwareWalletType.keystone: diff --git a/lib/new-ui/widgets/hardware_wallet/proceed_on_device_message.dart b/lib/new-ui/widgets/hardware_wallet/proceed_on_device_message.dart index 100325dbec..e60e39e634 100644 --- a/lib/new-ui/widgets/hardware_wallet/proceed_on_device_message.dart +++ b/lib/new-ui/widgets/hardware_wallet/proceed_on_device_message.dart @@ -51,6 +51,8 @@ class HardwareWalletProceedOnDeviceMessage extends StatelessWidget { return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg"; case HardwareWalletType.cupcake: return "assets/images/cupcake.svg"; + case HardwareWalletType.coldcardUsb: + return "assets/images/hardware_wallet/coldcard_man.svg"; case HardwareWalletType.coldcard: case HardwareWalletType.seedsigner: case HardwareWalletType.keystone: diff --git a/lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart b/lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart index 2a06f37e6b..a347a244ca 100644 --- a/lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart +++ b/lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart @@ -194,6 +194,8 @@ class _HardwareWalletProceedOnDeviceSheetState extends State { return 'assets/new-ui/hardware_wallets/device_trezor_safe_5.svg'; case HardwareWalletDeviceType.trezorSafe7: return 'assets/new-ui/hardware_wallets/device_trezor_safe_7.svg'; + case HardwareWalletDeviceType.coldcard: + return 'assets/images/hardware_wallet/coldcard_man.svg'; default: return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg'; diff --git a/lib/src/screens/connect_device/select_device_manufacturer_page.dart b/lib/src/screens/connect_device/select_device_manufacturer_page.dart index b9df3292d1..6977e95c07 100644 --- a/lib/src/screens/connect_device/select_device_manufacturer_page.dart +++ b/lib/src/screens/connect_device/select_device_manufacturer_page.dart @@ -5,7 +5,9 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/connect_device/widgets/dotted_divider.dart'; import 'package:cake_wallet/src/screens/connect_device/widgets/manufacturer_option_tile.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/permission_handler.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/restore/wallet_restore_from_qr_code.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; @@ -154,6 +156,11 @@ class SelectDeviceManufacturerPage extends BasePage { if (onSelect != null) return onSelect!.call(context, manufacturer.hardwareWalletType!); + if (manufacturer.hardwareWalletType == HardwareWalletType.coldcard && + Platform.isAndroid) { + return _onColdcardSelected(context); + } + if (isAirgappedWallet(manufacturer.hardwareWalletType)) { _onScanQRCode(context, manufacturer.hardwareWalletType!); } else if (manufacturer.hardwareWalletType != null) { @@ -200,6 +207,28 @@ class SelectDeviceManufacturerPage extends BasePage { bool isRestoring = false; + void _onColdcardSelected(BuildContext context) { + showPopUp( + context: context, + builder: (dialogContext) => AlertWithTwoActions( + alertTitle: S.of(context).select_manufacturer_title, + // TODO: localize these labels via .arb before release + alertContent: 'Connect over USB, or scan an airgapped QR code?', + leftButtonText: 'QR Code', + rightButtonText: 'USB', + actionLeftButton: () { + Navigator.of(dialogContext).pop(); + _onScanQRCode(context, HardwareWalletType.coldcard); + }, + actionRightButton: () { + Navigator.of(dialogContext).pop(); + Navigator.pushNamed(context, Routes.connectHardwareWallet, + arguments: [HardwareWalletType.coldcardUsb]); + }, + ), + ); + } + Future _onScanQRCode(BuildContext context, HardwareWalletType type) async { final isCameraPermissionGranted = await PermissionHandler.checkPermission(Permission.camera, context); diff --git a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart index 0f0d03a1a1..31e0d054dd 100644 --- a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart @@ -41,6 +41,8 @@ class CryptoBalanceWidget extends StatelessWidget { return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg"; case HardwareWalletType.cupcake: return "assets/images/cupcake.svg"; + case HardwareWalletType.coldcardUsb: + return "assets/images/hardware_wallet/coldcard_man.svg"; case HardwareWalletType.coldcard: case HardwareWalletType.seedsigner: case HardwareWalletType.keystone: diff --git a/lib/view_model/hardware_wallet/coldcard_view_model.dart b/lib/view_model/hardware_wallet/coldcard_view_model.dart new file mode 100644 index 0000000000..827ab0f497 --- /dev/null +++ b/lib/view_model/hardware_wallet/coldcard_view_model.dart @@ -0,0 +1,68 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; +import 'package:coldcard_protocol/client.dart'; +import 'package:coldcard_usb/coldcard_usb_transport.dart'; +import 'package:coldcard_usb/ledger_usb_platform_interface.dart'; +import 'package:cw_core/hardware/hardware_wallet_service.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; + +class ColdcardViewModel extends HardwareWalletViewModel { + HardwareWalletType get hardwareWalletType => HardwareWalletType.coldcardUsb; + bool get isBleEnabled => false; + bool get hasBluetooth => false; + bool _isConnecting = false; + + ColdCardDevice? _dev; + + bool isConnected(WalletType type) { + return _dev != null; + } + + Future updateBleState() async {} + + Stream scanForBleDevices() => const Stream.empty(); + + // only returns first coldcard device found + @override + Future> getAllUsbDevices() async { + final usbDevices = await ColdcardUsbPlatform.instance.getDevices(); + final found = usbDevices.any( + (d) => d.vendorId == ColdCardDevice.coinkiteVid && d.productId == ColdCardDevice.ckccPid); + return found ? [ColdcardHardwareWalletDevice()] : []; + } + + Future stopScanning() async {} + + Future connectDevice(HardwareWalletDevice device, WalletType type) async { + if (!(device is ColdcardHardwareWalletDevice)) return false; + if (_isConnecting) return false; + _isConnecting = true; + + try { + _dev = await ColdCardDevice.create(ColdcardUsbTransport()); + _isConnecting = false; + return true; + } catch (e) { + printV(e); + } + + _isConnecting = false; + return false; + } + + HardwareWalletService getHardwareWalletService(WalletType type) { + if (type != WalletType.bitcoin) throw UnimplementedError(); + return bitcoin!.getColdcardHardwareWalletService(_dev!); + } + + Future initWallet(WalletBase wallet) async { + return bitcoin!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type)); + } + + // todo: add proper error codes + String? interpretErrorCode(String error) => null; +} diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 9b2fa4dc9d..84e4102ceb 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -74,6 +74,7 @@ abstract class WalletKeysViewModelBase with Store { case HardwareWalletType.cupcake: return 'Cupcake'; case HardwareWalletType.coldcard: + case HardwareWalletType.coldcardUsb: return 'Coldcard'; case HardwareWalletType.seedsigner: return 'SeedSigner'; diff --git a/pubspec_base.yaml b/pubspec_base.yaml index c5525007bc..ef64864981 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -132,6 +132,14 @@ dependencies: ref: d1242cea90f84b00200e7bcab914a5af750e23fb bitbox_flutter: path: ./scripts/bitbox_flutter + coldcard_protocol: + git: + url: https://github.com/warp-records/coldcard-protocol + ref: 30b7ea705fc4cf1ab0aa5c51c3483fa2b60fe4f9 + coldcard_usb: + git: + url: https://github.com/warp-records/coldcard-usb + ref: 16b1115ab23dc21585f7f97ba06375c28fbe2ee7 hashlib: 1.19.2 on_chain: git: diff --git a/tool/configure.dart b/tool/configure.dart index a16d88f21a..c2815e4ef5 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -103,6 +103,7 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:coldcard_protocol/client.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/hardware/hardware_wallet_service.dart'; import 'package:cw_core/node.dart'; @@ -155,6 +156,7 @@ import 'package:cw_bitcoin/hardware/bitcoin_ledger_service.dart'; import 'package:cw_bitcoin/hardware/litecoin_ledger_service.dart'; import 'package:cw_bitcoin/hardware/bitbox_service.dart'; import 'package:cw_bitcoin/hardware/trezor_service.dart'; +import 'package:cw_bitcoin/hardware/coldcard_service.dart'; import 'package:mobx/mobx.dart'; import "package:breez_sdk_spark_flutter/src/rust/errors.dart"; """; @@ -274,6 +276,7 @@ abstract class Bitcoin { HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection, bool isBitcoin); HardwareWalletService getBitboxHardwareWalletService(bitbox.BitboxManager manager, bool isBitcoin); HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorConnect connect, bool isBitcoin); + HardwareWalletService getColdcardHardwareWalletService(ColdCardDevice device); List updateOutputs(PendingTransaction pendingTransaction, List outputs); bool txIsReceivedSilentPayment(TransactionInfo txInfo); bool txIsMweb(TransactionInfo txInfo);