Skip to content
Open
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
53 changes: 53 additions & 0 deletions cw_bitcoin/lib/hardware/coldcard_service.dart
Original file line number Diff line number Diff line change
@@ -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<Uint8List> getMasterFingerprint() async {
return Uint8List(4)..buffer.asByteData().setUint32(0, device.masterFingerprint!, Endian.little);
}

@override
Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5}) async {
final accounts = <HardwareAccountData>[];
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<Uint8List> signTransaction({required String transaction}) async =>
device.signTransaction(base64Decode(transaction), finalize: true);

@override
Future<Uint8List> signMessage({required Uint8List message, String? derivationPath}) async {
final result = await device.signMessage(message, subpath: derivationPath ?? 'm');
return result.$2;
}
}
8 changes: 8 additions & 0 deletions cw_bitcoin/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 12 additions & 5 deletions cw_core/lib/hardware/device_connection_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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:
Expand All @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions cw_core/lib/wallet_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ enum HardwareWalletType {
coldcard,
seedsigner,
keystone,
trezor;
trezor,
coldcardUsb;
}

enum WalletInfoAddressType {
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions cw_core/lib/wallet_info_legacy.part.dart

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

1 change: 1 addition & 0 deletions cw_evm/lib/evm_chain_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions lib/bitcoin/cw_bitcoin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,11 @@ class CWBitcoin extends Bitcoin {
return LitecoinTrezorService(connect);
}

@override
HardwareWalletService getColdcardHardwareWalletService(ColdCardDevice device) {
return ColdcardService(device);
}

@override
List<ElectrumSubAddress> getSilentPaymentAddresses(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
Expand Down
4 changes: 4 additions & 0 deletions lib/di.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -415,6 +416,7 @@ Future<void> setup({
case HardwareWalletType.bitbox: return getIt<BitboxViewModel>();
case HardwareWalletType.ledger: return getIt<LedgerViewModel>();
case HardwareWalletType.trezor: return getIt<TrezorConnectViewModel>();
case HardwareWalletType.coldcardUsb: return getIt<ColdcardViewModel>();
case HardwareWalletType.cupcake:
case HardwareWalletType.coldcard:
case HardwareWalletType.seedsigner:
Expand All @@ -426,6 +428,8 @@ Future<void> setup({
getIt.registerLazySingleton(() => LedgerViewModel());

getIt.registerLazySingleton(() => BitboxViewModel());

getIt.registerLazySingleton(() => ColdcardViewModel());

getIt.registerLazySingleton(() => TrezorConnect("cakewallet://trezor_connect",
appName: "Cake Wallet"));
Expand Down
9 changes: 8 additions & 1 deletion lib/entities/hardware_wallet/hardware_wallet_device.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -71,7 +77,8 @@ enum HardwareWalletDeviceType {
trezorModelT,
trezorSafe3,
trezorSafe5,
trezorSafe7;
trezorSafe7,
coldcard;
}

enum HardwareWalletConnectionType {
Expand Down
2 changes: 2 additions & 0 deletions lib/new-ui/widgets/coins_page/wallet_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
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:
Expand Down
2 changes: 2 additions & 0 deletions lib/src/screens/connect_device/connect_device_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -200,6 +207,28 @@ class SelectDeviceManufacturerPage extends BasePage {

bool isRestoring = false;

void _onColdcardSelected(BuildContext context) {
showPopUp<void>(
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<void> _onScanQRCode(BuildContext context, HardwareWalletType type) async {
final isCameraPermissionGranted =
await PermissionHandler.checkPermission(Permission.camera, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
68 changes: 68 additions & 0 deletions lib/view_model/hardware_wallet/coldcard_view_model.dart
Original file line number Diff line number Diff line change
@@ -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<void> updateBleState() async {}

Stream<HardwareWalletDevice> scanForBleDevices() => const Stream.empty();

// only returns first coldcard device found
@override
Future<List<HardwareWalletDevice>> 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<void> stopScanning() async {}

Future<bool> 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<void> initWallet(WalletBase wallet) async {
return bitcoin!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
}

// todo: add proper error codes
String? interpretErrorCode(String error) => null;
}
1 change: 1 addition & 0 deletions lib/view_model/wallet_keys_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading