This commit is contained in:
cyan 2025-06-27 11:40:33 +00:00 committed by GitHub
commit 1fc440f53c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 478 additions and 155 deletions

View file

@ -55,7 +55,7 @@ jobs:
- name: Flutter action - name: Flutter action
uses: subosito/flutter-action@v1 uses: subosito/flutter-action@v1
with: with:
flutter-version: "3.27.0" flutter-version: "3.27.4"
channel: stable channel: stable
- name: Install package dependencies - name: Install package dependencies

View file

@ -9,7 +9,7 @@ jobs:
PR_test_build: PR_test_build:
runs-on: linux-amd64 runs-on: linux-amd64
container: container:
image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly
env: env:
STORE_PASS: test@cake_wallet STORE_PASS: test@cake_wallet
KEY_PASS: test@cake_wallet KEY_PASS: test@cake_wallet

View file

@ -9,7 +9,7 @@ jobs:
PR_test_build: PR_test_build:
runs-on: linux-amd64 runs-on: linux-amd64
container: container:
image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly
env: env:
STORE_PASS: test@cake_wallet STORE_PASS: test@cake_wallet
KEY_PASS: test@cake_wallet KEY_PASS: test@cake_wallet

View file

@ -1,4 +1,4 @@
# docker buildx build --push --pull --platform linux/amd64,linux/arm64 . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly # docker buildx build --push --pull --platform linux/amd64,linux/arm64 . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly
# Heavily inspired by cirrusci images # Heavily inspired by cirrusci images
# https://github.com/cirruslabs/docker-images-android/blob/master/sdk/tools/Dockerfile # https://github.com/cirruslabs/docker-images-android/blob/master/sdk/tools/Dockerfile
@ -15,7 +15,7 @@ LABEL org.opencontainers.image.source=https://github.com/cake-tech/cake_wallet
ENV GOLANG_VERSION=1.24.1 ENV GOLANG_VERSION=1.24.1
# Pin Flutter version to latest known-working version # Pin Flutter version to latest known-working version
ENV FLUTTER_VERSION=3.27.0 ENV FLUTTER_VERSION=3.27.4
# Pin Android Studio, platform, and build tools versions to latest known-working version # Pin Android Studio, platform, and build tools versions to latest known-working version
# Comes from https://developer.android.com/studio/#command-tools # Comes from https://developer.android.com/studio/#command-tools

View file

@ -28,6 +28,16 @@ class BitcoinReceivePageOption implements ReceivePageOption {
BitcoinReceivePageOption.p2pkh BitcoinReceivePageOption.p2pkh
]; ];
static const allViewOnly = [
BitcoinReceivePageOption.p2wpkh,
// TODO: uncomment this after we properly derive keys and not use m/84 for
// all of them (as this breaks cupcake)
// BitcoinReceivePageOption.p2tr,
// BitcoinReceivePageOption.p2wsh,
// BitcoinReceivePageOption.p2sh,
// BitcoinReceivePageOption.p2pkh
];
static const allLitecoin = [ static const allLitecoin = [
BitcoinReceivePageOption.p2wpkh, BitcoinReceivePageOption.p2wpkh,
BitcoinReceivePageOption.mweb, BitcoinReceivePageOption.mweb,

View file

@ -24,6 +24,7 @@ import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_keys_file.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -32,6 +33,9 @@ import 'package:ledger_bitcoin/ledger_bitcoin.dart';
import 'package:ledger_bitcoin/psbt.dart'; import 'package:ledger_bitcoin/psbt.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:ur/cbor_lite.dart';
import 'package:ur/ur.dart';
import 'package:ur/ur_decoder.dart';
part 'bitcoin_wallet.g.dart'; part 'bitcoin_wallet.g.dart';
@ -352,7 +356,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
as PendingBitcoinTransaction; as PendingBitcoinTransaction;
final payjoinUri = credentials.payjoinUri; final payjoinUri = credentials.payjoinUri;
if (payjoinUri == null) return tx; if (payjoinUri == null && !tx.shouldCommitUR()) return tx;
final transaction = await buildPsbt( final transaction = await buildPsbt(
utxos: tx.utxos, utxos: tx.utxos,
@ -370,14 +374,19 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
outputOrdering: BitcoinOrdering.none, outputOrdering: BitcoinOrdering.none,
enableRBF: true, enableRBF: true,
publicKeys: tx.publicKeys!, publicKeys: tx.publicKeys!,
masterFingerprint: Uint8List(0)); masterFingerprint: Uint8List.fromList([0, 0, 0, 0]));
final originalPsbt = await signPsbt( if (tx.shouldCommitUR()) {
base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys()); tx.unsignedPsbt = transaction.asPsbtV0();
return tx;
}
final originalPsbt =
await signPsbt(base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys());
tx.commitOverride = () async { tx.commitOverride = () async {
final sender = await payjoinManager.initSender( final sender =
payjoinUri, originalPsbt, int.parse(tx.feeRate)); await payjoinManager.initSender(payjoinUri!, originalPsbt, int.parse(tx.feeRate));
payjoinManager.spawnNewSender( payjoinManager.spawnNewSender(
sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount)); sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount));
}; };
@ -405,6 +414,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
feeRate: "", feeRate: "",
network: network, network: network,
hasChange: true, hasChange: true,
isViewOnly: false,
).commit(); ).commit();
} }
@ -430,6 +440,44 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
return base64Encode(psbt.asPsbtV0()); return base64Encode(psbt.asPsbtV0());
} }
Future<void> commitPsbtUR(List<String> urCodes) async {
if (urCodes.isEmpty) throw Exception("No QR code got scanned");
bool isUr = urCodes.any((str) {
return str.startsWith("ur:psbt/");
});
if (isUr) {
final ur = URDecoder();
for (final inp in urCodes) {
ur.receivePart(inp);
}
final result = (ur.result as UR);
final cbor = result.cbor;
final cborDecoder = CBORDecoder(cbor);
final out = cborDecoder.decodeBytes();
final bytes = out.$1;
final base64psbt = base64Encode(bytes);
final psbt = PsbtV2()..deserializeV0(base64Decode(base64psbt));
// psbt.finalize();
final finalized = base64Encode(psbt.serialize());
await commitPsbt(finalized);
} else {
final btcTx = BtcTransaction.fromRaw(urCodes.first);
return PendingBitcoinTransaction(
btcTx,
type,
electrumClient: electrumClient,
amount: 0,
fee: 0,
feeRate: "",
network: network,
hasChange: true,
isViewOnly: false,
).commit();
}
}
@override @override
Future<String> signMessage(String message, {String? address = null}) async { Future<String> signMessage(String message, {String? address = null}) async {
if (walletInfo.isHardwareWallet) { if (walletInfo.isHardwareWallet) {

View file

@ -54,6 +54,17 @@ class BitcoinRestoreWalletFromWIFCredentials extends WalletCredentials {
final String wif; final String wif;
} }
class BitcoinWalletFromKeysCredentials extends WalletCredentials {
BitcoinWalletFromKeysCredentials({
required String name,
required String password,
required this.xpub,
WalletInfo? walletInfo,
}) : super(name: name, password: password, walletInfo: walletInfo);
final String xpub;
}
class BitcoinRestoreWalletFromHardware extends WalletCredentials { class BitcoinRestoreWalletFromHardware extends WalletCredentials {
BitcoinRestoreWalletFromHardware({ BitcoinRestoreWalletFromHardware({
required String name, required String name,

View file

@ -1,7 +1,15 @@
class BitcoinWalletKeys { class BitcoinWalletKeys {
const BitcoinWalletKeys({required this.wif, required this.privateKey, required this.publicKey}); const BitcoinWalletKeys({required this.wif, required this.privateKey, required this.publicKey, required this.xpub});
final String wif; final String wif;
final String privateKey; final String privateKey;
final String publicKey; final String publicKey;
final String xpub;
Map<String, String> toJson() => {
'wif': wif,
'privateKey': privateKey,
'publicKey': publicKey,
'xpub': xpub
};
} }

View file

@ -20,7 +20,7 @@ import 'package:bip39/bip39.dart' as bip39;
class BitcoinWalletService extends WalletService< class BitcoinWalletService extends WalletService<
BitcoinNewWalletCredentials, BitcoinNewWalletCredentials,
BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials, BitcoinWalletFromKeysCredentials,
BitcoinRestoreWalletFromHardware> { BitcoinRestoreWalletFromHardware> {
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource,
this.payjoinSessionSource, this.alwaysScan, this.isDirect); this.payjoinSessionSource, this.alwaysScan, this.isDirect);
@ -169,9 +169,25 @@ class BitcoinWalletService extends WalletService<
} }
@override @override
Future<BitcoinWallet> restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials, Future<BitcoinWallet> restoreFromKeys(BitcoinWalletFromKeysCredentials credentials,
{bool? isTestnet}) async => {bool? isTestnet}) async {
throw UnimplementedError(); final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
credentials.walletInfo?.network = network.value;
final wallet = await BitcoinWallet(
password: credentials.password!,
xpub: credentials.xpub,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
networkParam: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
payjoinBox: payjoinSessionSource,
);
await wallet.save();
await wallet.init();
return wallet;
}
@override @override
Future<BitcoinWallet> restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials, Future<BitcoinWallet> restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials,

View file

@ -280,11 +280,26 @@ abstract class ElectrumWalletBase
} }
@override @override
BitcoinWalletKeys get keys => BitcoinWalletKeys( BitcoinWalletKeys get keys {
wif: WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer), String? wif;
privateKey: hd.privateKey.toHex(), String? privateKey;
publicKey: hd.publicKey.toHex(), String? publicKey;
); try {
wif = WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer);
} catch (_) {}
try {
privateKey = hd.privateKey.toHex();
} catch (_) {}
try {
publicKey = hd.publicKey.toHex();
} catch (_) {}
return BitcoinWalletKeys(
wif: wif ?? '',
privateKey: privateKey ?? '',
publicKey: publicKey ?? '',
xpub: xpub,
);
}
String _password; String _password;
List<BitcoinUnspent> unspentCoins; List<BitcoinUnspent> unspentCoins;
@ -666,7 +681,7 @@ abstract class ElectrumWalletBase
); );
spendsSilentPayment = true; spendsSilentPayment = true;
isSilentPayment = true; isSilentPayment = true;
} else if (!isHardwareWallet) { } else if (!isHardwareWallet && keys.privateKey.isNotEmpty) {
privkey = privkey =
generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network); generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network);
} }
@ -1104,7 +1119,8 @@ abstract class ElectrumWalletBase
network: network, network: network,
hasChange: estimatedTx.hasChange, hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll, isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot,
isViewOnly: false,
)..addListener((transaction) async { )..addListener((transaction) async {
transactionHistory.addOne(transaction); transactionHistory.addOne(transaction);
await updateBalance(); await updateBalance();
@ -1138,6 +1154,7 @@ abstract class ElectrumWalletBase
bool hasTaprootInputs = false; bool hasTaprootInputs = false;
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
if (keys.privateKey.isEmpty) return "";
String error = "Cannot find private key."; String error = "Cannot find private key.";
ECPrivateInfo? key; ECPrivateInfo? key;
@ -1175,18 +1192,21 @@ abstract class ElectrumWalletBase
} }
}); });
return PendingBitcoinTransaction(transaction, type, return PendingBitcoinTransaction(
electrumClient: electrumClient, transaction,
amount: estimatedTx.amount, type,
fee: estimatedTx.fee, electrumClient: electrumClient,
feeRate: feeRateInt.toString(), amount: estimatedTx.amount,
network: network, fee: estimatedTx.fee,
hasChange: estimatedTx.hasChange, feeRate: feeRateInt.toString(),
isSendAll: estimatedTx.isSendAll, network: network,
hasTaprootInputs: hasTaprootInputs, hasChange: estimatedTx.hasChange,
utxos: estimatedTx.utxos, isSendAll: estimatedTx.isSendAll,
publicKeys: estimatedTx.publicKeys) hasTaprootInputs: hasTaprootInputs,
..addListener((transaction) async { utxos: estimatedTx.utxos,
publicKeys: estimatedTx.publicKeys,
isViewOnly: keys.privateKey.isEmpty,
)..addListener((transaction) async {
transactionHistory.addOne(transaction); transactionHistory.addOne(transaction);
if (estimatedTx.spendsSilentPayment) { if (estimatedTx.spendsSilentPayment) {
transactionHistory.transactions.values.forEach((tx) { transactionHistory.transactions.values.forEach((tx) {
@ -1847,6 +1867,7 @@ abstract class ElectrumWalletBase
network: network, network: network,
hasChange: changeOutputs.isNotEmpty, hasChange: changeOutputs.isNotEmpty,
feeRate: newFee.toString(), feeRate: newFee.toString(),
isViewOnly: keys.privateKey.isEmpty,
)..addListener((transaction) async { )..addListener((transaction) async {
transactionHistory.transactions.values.forEach((tx) { transactionHistory.transactions.values.forEach((tx) {
if (tx.id == hash) { if (tx.id == hash) {

View file

@ -1,3 +1,7 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:bbqrdart/bbqrdart.dart';
import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:grpc/grpc.dart'; import 'package:grpc/grpc.dart';
import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/exceptions.dart';
@ -11,6 +15,9 @@ import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:cw_mweb/cw_mweb.dart'; import 'package:cw_mweb/cw_mweb.dart';
import 'package:cw_mweb/mwebd.pb.dart'; import 'package:cw_mweb/mwebd.pb.dart';
import 'package:ur/cbor_lite.dart';
import 'package:ur/ur.dart';
import 'package:ur/ur_encoder.dart';
class PendingBitcoinTransaction with PendingTransaction { class PendingBitcoinTransaction with PendingTransaction {
PendingBitcoinTransaction( PendingBitcoinTransaction(
@ -28,6 +35,8 @@ class PendingBitcoinTransaction with PendingTransaction {
this.utxos = const [], this.utxos = const [],
this.publicKeys, this.publicKeys,
this.commitOverride, this.commitOverride,
this.unsignedPsbt,
required this.isViewOnly,
}) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[]; }) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type; final WalletType type;
@ -40,6 +49,7 @@ class PendingBitcoinTransaction with PendingTransaction {
final bool isSendAll; final bool isSendAll;
final bool hasChange; final bool hasChange;
final bool hasTaprootInputs; final bool hasTaprootInputs;
final bool isViewOnly;
List<UtxoWithAddress> utxos; List<UtxoWithAddress> utxos;
bool isMweb; bool isMweb;
String? changeAddressOverride; String? changeAddressOverride;
@ -49,6 +59,8 @@ class PendingBitcoinTransaction with PendingTransaction {
final Map<String, PublicKeyWithDerivationPath>? publicKeys; final Map<String, PublicKeyWithDerivationPath>? publicKeys;
Future<void> Function()? commitOverride; Future<void> Function()? commitOverride;
Uint8List? unsignedPsbt;
@override @override
String get id => idOverride ?? _tx.txId(); String get id => idOverride ?? _tx.txId();
@ -72,9 +84,11 @@ class PendingBitcoinTransaction with PendingTransaction {
try { try {
final change = _tx.outputs.firstWhere((out) => out.isChange); final change = _tx.outputs.firstWhere((out) => out.isChange);
if (changeAddressOverride != null) { if (changeAddressOverride != null) {
return PendingChange(changeAddressOverride!, BtcUtils.fromSatoshi(change.amount)); return PendingChange(
changeAddressOverride!, BtcUtils.fromSatoshi(change.amount));
} }
return PendingChange(change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount)); return PendingChange(
change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount));
} catch (_) { } catch (_) {
return null; return null;
} }
@ -123,12 +137,14 @@ class PendingBitcoinTransaction with PendingTransaction {
Future<void> _ltcCommit() async { Future<void> _ltcCommit() async {
try { try {
final resp = await CwMweb.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); final resp = await CwMweb.broadcast(
BroadcastRequest(rawTx: BytesUtils.fromHexString(hex)));
idOverride = resp.txid; idOverride = resp.txid;
} on GrpcError catch (e) { } on GrpcError catch (e) {
throw BitcoinTransactionCommitFailed(errorMessage: e.message); throw BitcoinTransactionCommitFailed(errorMessage: e.message);
} catch (e) { } catch (e) {
throw BitcoinTransactionCommitFailed(errorMessage: "Unknown error: ${e.toString()}"); throw BitcoinTransactionCommitFailed(
errorMessage: "Unknown error: ${e.toString()}");
} }
} }
@ -147,7 +163,8 @@ class PendingBitcoinTransaction with PendingTransaction {
_listeners.forEach((listener) => listener(transactionInfo())); _listeners.forEach((listener) => listener(transactionInfo()));
} }
void addListener(void Function(ElectrumTransactionInfo transaction) listener) => void addListener(
void Function(ElectrumTransactionInfo transaction) listener) =>
_listeners.add(listener); _listeners.add(listener);
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type, ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,
@ -164,7 +181,38 @@ class PendingBitcoinTransaction with PendingTransaction {
fee: fee); fee: fee);
@override @override
Future<String?> commitUR() { bool shouldCommitUR() => isViewOnly;
throw UnimplementedError();
@override
Future<Map<String, String>> commitUR() {
var sourceBytes = unsignedPsbt!;
var cborEncoder = CBOREncoder();
cborEncoder.encodeBytes(sourceBytes);
var ur = UR("psbt", cborEncoder.getBytes());
var urLegacy = UR("crypto-psbt", cborEncoder.getBytes());
// var ur = UR("psbt", Uint8List.fromList(List.generate(64*1024, (int x) => x % 256)));
var encoded = UREncoder(ur, 120);
var encodedLegacy = UREncoder(urLegacy, 120);
List<String> values = [];
List<String> valuesLegacy = [];
while (!encoded.isComplete) {
values.add(encoded.nextPart());
valuesLegacy.add(encodedLegacy.nextPart());
}
final bbqrObj = BBQRPsbt.fromUint8List(sourceBytes);
List<String> bbqr = [
bbqrObj.asString(),
];
while (!bbqrObj.isDone) {
bbqrObj.next();
bbqr.add(bbqrObj.asString());
}
return Future.value({
"PSBT (bcur)": values.join("\n"),
"PSBT (bbqr)": bbqr.join("\n"),
"PSBT (bcur legacy)": valuesLegacy.join("\n"),
});
} }
} }

View file

@ -46,6 +46,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" version: "2.11.0"
bbqrdart:
dependency: "direct main"
description:
path: "."
ref: b0f3f72911221bafed0ab77359fccd64bc79d524
resolved-ref: b0f3f72911221bafed0ab77359fccd64bc79d524
url: "https://github.com/mrcyjanek/bbqrdart"
source: git
version: "1.0.0"
bech32: bech32:
dependency: "direct main" dependency: "direct main"
description: description:
@ -588,8 +597,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "packages/ledger-bitcoin" path: "packages/ledger-bitcoin"
ref: trunk ref: eab179d487cddda3f647f6608115a89662facde4
resolved-ref: e93254f3ff3f996fb91f65a1e7ceffb9f510b4c8 resolved-ref: eab179d487cddda3f647f6608115a89662facde4
url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" url: "https://github.com/cake-tech/ledger-flutter-plus-plugins"
source: git source: git
version: "0.0.3" version: "0.0.3"
@ -1101,6 +1110,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.0" version: "0.3.0"
ur:
dependency: "direct main"
description:
path: "."
ref: "5738f70d0ec3d50977ac3dd01fed62939600238b"
resolved-ref: "5738f70d0ec3d50977ac3dd01fed62939600238b"
url: "https://github.com/bukata-sa/bc-ur-dart"
source: git
version: "0.1.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -1182,5 +1200,5 @@ packages:
source: hosted source: hosted
version: "2.2.2" version: "2.2.2"
sdks: sdks:
dart: ">=3.6.0 <4.0.0" dart: ">=3.6.2 <4.0.0"
flutter: ">=3.27.0" flutter: ">=3.27.0"

View file

@ -49,7 +49,7 @@ dependencies:
git: git:
url: https://github.com/cake-tech/ledger-flutter-plus-plugins url: https://github.com/cake-tech/ledger-flutter-plus-plugins
path: packages/ledger-bitcoin path: packages/ledger-bitcoin
ref: trunk ref: eab179d487cddda3f647f6608115a89662facde4
ledger_litecoin: ledger_litecoin:
git: git:
url: https://github.com/cake-tech/ledger-flutter-plus-plugins url: https://github.com/cake-tech/ledger-flutter-plus-plugins
@ -58,6 +58,14 @@ dependencies:
git: git:
url: https://github.com/sneurlax/socks_socket url: https://github.com/sneurlax/socks_socket
ref: e6232c53c1595469931ababa878759a067c02e94 ref: e6232c53c1595469931ababa878759a067c02e94
ur:
git:
url: https://github.com/bukata-sa/bc-ur-dart
ref: 5738f70d0ec3d50977ac3dd01fed62939600238b
bbqrdart:
git:
url: https://github.com/mrcyjanek/bbqrdart
ref: b0f3f72911221bafed0ab77359fccd64bc79d524
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -86,7 +86,7 @@ class PendingBitcoinCashTransaction with PendingTransaction {
isReplaced: false, isReplaced: false,
); );
@override @override
Future<String?> commitUR() { Future<Map<String, String>> commitUR() {
throw UnimplementedError(); throw UnimplementedError();
} }
} }

View file

@ -17,5 +17,5 @@ mixin PendingTransaction {
bool shouldCommitUR() => false; bool shouldCommitUR() => false;
Future<void> commit(); Future<void> commit();
Future<String?> commitUR(); Future<Map<String, String>> commitUR();
} }

View file

@ -829,4 +829,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.6.0 <4.0.0" dart: ">=3.6.0 <4.0.0"
flutter: ">=3.27.0" flutter: ">=3.27.4"

View file

@ -33,7 +33,7 @@ class DecredPendingTransaction with PendingTransaction {
} }
@override @override
Future<String?> commitUR() { Future<Map<String, String>> commitUR() {
throw UnimplementedError(); throw UnimplementedError();
} }
} }

View file

@ -52,7 +52,7 @@ class PendingEVMChainTransaction with PendingTransaction {
} }
@override @override
Future<String?> commitUR() { Future<Map<String, String>> commitUR() {
throw UnimplementedError(); throw UnimplementedError();
} }
} }

View file

@ -39,6 +39,7 @@ class PendingMoneroTransaction with PendingTransaction {
String get feeFormatted => AmountConverter.amountIntToString( String get feeFormatted => AmountConverter.amountIntToString(
CryptoCurrency.xmr, pendingTransactionDescription.fee); CryptoCurrency.xmr, pendingTransactionDescription.fee);
@override
bool shouldCommitUR() => isViewOnly; bool shouldCommitUR() => isViewOnly;
@override @override
@ -64,7 +65,7 @@ class PendingMoneroTransaction with PendingTransaction {
} }
@override @override
Future<String?> commitUR() async { Future<Map<String, String>> commitUR() async {
try { try {
final ret = await monero_transaction_history.commitTransactionFromPointerAddress( final ret = await monero_transaction_history.commitTransactionFromPointerAddress(
address: pendingTransactionDescription.pointerAddress, address: pendingTransactionDescription.pointerAddress,
@ -74,7 +75,10 @@ class PendingMoneroTransaction with PendingTransaction {
await Future.delayed(const Duration(milliseconds: 250)); await Future.delayed(const Duration(milliseconds: 250));
await wallet.fetchTransactions(); await wallet.fetchTransactions();
}()); }());
return ret; if (ret == null) return {};
return {
"xmr-txsigned": ret,
};
} catch (e) { } catch (e) {
final message = e.toString(); final message = e.toString();

View file

@ -39,7 +39,7 @@ class PendingNanoTransaction with PendingTransaction {
} }
@override @override
Future<String?> commitUR() { Future<Map<String, String>> commitUR() {
throw UnimplementedError(); throw UnimplementedError();
} }
} }

View file

@ -978,4 +978,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.6.0 <4.0.0" dart: ">=3.6.0 <4.0.0"
flutter: ">=3.27.0" flutter: ">=3.27.4"

View file

@ -41,7 +41,7 @@ class PendingSolanaTransaction with PendingTransaction {
String get id => ''; String get id => '';
@override @override
Future<String?> commitUR() { Future<Map<String, String>> commitUR() {
throw UnimplementedError(); throw UnimplementedError();
} }
} }

View file

@ -32,7 +32,7 @@ class PendingTronTransaction with PendingTransaction {
String get id => ''; String get id => '';
@override @override
Future<String?> commitUR() { Future<Map<String, String>> commitUR() {
throw UnimplementedError(); throw UnimplementedError();
} }
} }

View file

@ -52,7 +52,7 @@ class PendingWowneroTransaction with PendingTransaction {
} }
@override @override
Future<String?> commitUR() { Future<Map<String, String>> commitUR() {
throw UnimplementedError(); throw UnimplementedError();
} }
} }

View file

@ -46,7 +46,7 @@ class PendingZanoTransaction with PendingTransaction {
} }
@override @override
Future<String?> commitUR() { Future<Map<String, String>> commitUR() {
throw UnimplementedError(); throw UnimplementedError();
} }
} }

View file

@ -18,8 +18,8 @@ In order to build the latest version of Cake Wallet, simply run the following:
git clone --branch main https://github.com/cake-tech/cake_wallet.git git clone --branch main https://github.com/cake-tech/cake_wallet.git
# NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest. # NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest.
cd cake_wallet cd cake_wallet
# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry # docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry
docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly bash -x << EOF docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly bash -x << EOF
set -x -e set -x -e
pushd scripts/android pushd scripts/android
source ./app_env.sh cakewallet source ./app_env.sh cakewallet

View file

@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your iOS devi
```txt ```txt
macOS 15.3.1 macOS 15.3.1
Xcode 16.2 Xcode 16.2
Flutter 3.27.0 Flutter 3.27.4
``` ```
NOTE: Newer versions of macOS and Xcode may also work, but have not been confirmed to work by the Cake team. NOTE: Newer versions of macOS and Xcode may also work, but have not been confirmed to work by the Cake team.
@ -43,9 +43,9 @@ To enable iOS build support for Xcode, perform the following:
### 3. Installing Flutter ### 3. Installing Flutter
Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download).
NOTE: as `3.27.0` is not the latest version, you'll need to download it from <https://docs.flutter.dev/release/archive> instead of the link in the docs above. NOTE: as `3.27.4` is not the latest version, you'll need to download it from <https://docs.flutter.dev/release/archive> instead of the link in the docs above.
### 4. Installing Rust ### 4. Installing Rust
@ -65,7 +65,7 @@ The output of this command should appear like this, indicating successful instal
```zsh ```zsh
Doctor summary (to see all details, run flutter doctor -v): Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.27.0, on macOS 15.x.x) [✓] Flutter (Channel stable, 3.27.4, on macOS 15.x.x)
[✓] Xcode - develop for iOS and macOS (Xcode 16.2) [✓] Xcode - develop for iOS and macOS (Xcode 16.2)
``` ```

View file

@ -20,8 +20,8 @@ In order to build the latest version of Cake Wallet, simply run the following:
git clone --branch main https://github.com/cake-tech/cake_wallet.git git clone --branch main https://github.com/cake-tech/cake_wallet.git
# NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest. # NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest.
cd cake_wallet cd cake_wallet
# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry # docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry
docker run --privileged -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly bash -x << EOF docker run --privileged -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly bash -x << EOF
set -x -e set -x -e
pushd scripts pushd scripts
./gen_android_manifest.sh ./gen_android_manifest.sh

View file

@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your macOS de
```txt ```txt
macOS 15.3.1 macOS 15.3.1
Xcode 16.2 Xcode 16.2
Flutter 3.27.0 Flutter 3.27.4
``` ```
### 1. Installing dependencies ### 1. Installing dependencies
@ -34,9 +34,9 @@ sudo xcodebuild -runFirstLaunch
### 3. Installing Flutter ### 3. Installing Flutter
Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download).
NOTE: as `3.27.0` is not the latest version, you'll need to download it from <https://docs.flutter.dev/release/archive> instead of the link in the docs above. NOTE: as `3.27.4` is not the latest version, you'll need to download it from <https://docs.flutter.dev/release/archive> instead of the link in the docs above.
### 4. Installing Rust ### 4. Installing Rust
@ -56,7 +56,7 @@ The output of this command should appear like this, indicating successful instal
```zsh ```zsh
Doctor summary (to see all details, run flutter doctor -v): Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.27.0, on macOS 15.x.x) [✓] Flutter (Channel stable, 3.27.4, on macOS 15.x.x)
... ...
[✓] Xcode - develop for iOS and macOS (Xcode 16.2) [✓] Xcode - develop for iOS and macOS (Xcode 16.2)
... ...

View file

@ -6,18 +6,18 @@ The following are the system requirements to build Cake Wallet for your Windows
```txt ```txt
Windows 10 or later (64-bit), x86-64 based Windows 10 or later (64-bit), x86-64 based
Flutter 3.27.0 Flutter 3.27.4
``` ```
### 1. Installing Flutter ### 1. Installing Flutter
Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/windows). Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/windows).
In order for Flutter to function, you'll also need to enable Developer Mode: In order for Flutter to function, you'll also need to enable Developer Mode:
Start Menu > search for "Run" > type `ms-settings:developers`, and turn on Developer Mode. Start Menu > search for "Run" > type `ms-settings:developers`, and turn on Developer Mode.
NOTE: as `3.27.0` is not the latest version, you'll need to download it from <https://docs.flutter.dev/release/archive> instead of the link in the docs above. NOTE: as `3.27.4` is not the latest version, you'll need to download it from <https://docs.flutter.dev/release/archive> instead of the link in the docs above.
### 2. Install Development Tools ### 2. Install Development Tools

View file

@ -18,6 +18,14 @@ class CWBitcoin extends Bitcoin {
passphrase: passphrase, passphrase: passphrase,
); );
@override
WalletCredentials createBitcoinWalletFromKeys({
required String name,
required String password,
required String xpub,
}) =>
BitcoinWalletFromKeysCredentials(name: name, password: password, xpub: xpub);
@override @override
WalletCredentials createBitcoinRestoreWalletFromWIFCredentials( WalletCredentials createBitcoinRestoreWalletFromWIFCredentials(
{required String name, {required String name,
@ -62,11 +70,7 @@ class CWBitcoin extends Bitcoin {
final bitcoinWallet = wallet as ElectrumWallet; final bitcoinWallet = wallet as ElectrumWallet;
final keys = bitcoinWallet.keys; final keys = bitcoinWallet.keys;
return <String, String>{ return bitcoinWallet.keys.toJson();
'wif': keys.wif,
'privateKey': keys.privateKey,
'publicKey': keys.publicKey
};
} }
@override @override
@ -276,7 +280,14 @@ class CWBitcoin extends Bitcoin {
} }
@override @override
List<ReceivePageOption> getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all; List<ReceivePageOption> getBitcoinReceivePageOptions(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
final keys = bitcoinWallet.keys;
if (keys.privateKey.isEmpty) {
return BitcoinReceivePageOption.allViewOnly;
}
return BitcoinReceivePageOption.all;
}
@override @override
List<ReceivePageOption> getLitecoinReceivePageOptions() { List<ReceivePageOption> getLitecoinReceivePageOptions() {
@ -719,6 +730,12 @@ class CWBitcoin extends Bitcoin {
} }
} }
@override
Future<void> commitPsbtUR(Object wallet, List<String> urCodes) {
final _wallet = wallet as BitcoinWalletBase;
return _wallet.commitPsbtUR(urCodes);
}
@override @override
String getPayjoinEndpoint(Object wallet) { String getPayjoinEndpoint(Object wallet) {
final _wallet = wallet as ElectrumWallet; final _wallet = wallet as ElectrumWallet;

View file

@ -971,7 +971,7 @@ Future<void> setup({
getIt.registerFactory(() => AnimatedURModel(getIt.get<AppStore>())); getIt.registerFactory(() => AnimatedURModel(getIt.get<AppStore>()));
getIt.registerFactoryParam<AnimatedURPage, String, void>((String urQr, _) => getIt.registerFactoryParam<AnimatedURPage, Map<String, String>, void>((Map<String, String> urQr, _) =>
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr)); AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>( getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>(

View file

@ -198,14 +198,14 @@ class WalletRestoreFromKeysFormState extends State<WalletRestoreFromKeysForm> {
Widget _restoreFromKeysFormFields() { Widget _restoreFromKeysFormFields() {
// Decred can only restore a view only wallet with an account pubkey. Other // Decred can only restore a view only wallet with an account pubkey. Other
// fields are not used. // fields are not used.
if (widget.walletRestoreViewModel.type == WalletType.decred) { if (widget.walletRestoreViewModel.onlyViewKeyRestore) {
return Column( return Column(
children: [ children: [
BaseTextFormField( BaseTextFormField(
controller: viewKeyController, controller: viewKeyController,
hintText: S.of(context).view_key_public, hintText: S.of(context).view_key_public,
maxLines: null, maxLines: null,
) ),
], ],
); );
} }
@ -253,13 +253,14 @@ class WalletRestoreFromKeysFormState extends State<WalletRestoreFromKeysForm> {
maxLines: null, maxLines: null,
), ),
), ),
BlockchainHeightWidget( if (widget.walletRestoreViewModel.hasBlockchainHeightSelector)
key: blockchainHeightKey, BlockchainHeightWidget(
hasDatePicker: widget.walletRestoreViewModel.type != WalletType.haven, key: blockchainHeightKey,
onHeightChange: (_) => null, hasDatePicker: widget.walletRestoreViewModel.type != WalletType.haven,
onHeightOrDateEntered: widget.onHeightOrDateEntered, onHeightChange: (_) => null,
walletType: widget.walletRestoreViewModel.type, onHeightOrDateEntered: widget.onHeightOrDateEntered,
), walletType: widget.walletRestoreViewModel.type,
),
], ],
); );
} }

View file

@ -199,7 +199,7 @@ class WalletRestorePage extends BasePage {
credentials['seed'] = credentials['seed'] =
walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text; walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text;
if (walletRestoreViewModel.hasBlockchainHeightLanguageSelector) { if (walletRestoreViewModel.hasBlockchainHeightSelector) {
credentials['height'] = credentials['height'] =
walletRestoreFromSeedFormKey.currentState!.blockchainHeightKey.currentState?.height ?? walletRestoreFromSeedFormKey.currentState!.blockchainHeightKey.currentState?.height ??
-1; -1;
@ -219,7 +219,7 @@ class WalletRestorePage extends BasePage {
credentials['name'] = credentials['name'] =
walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text; walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text;
credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text; credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text;
if (walletRestoreViewModel.type != WalletType.decred) { if (!walletRestoreViewModel.onlyViewKeyRestore) {
credentials['address'] = credentials['address'] =
walletRestoreFromKeysFormKey.currentState!.addressController.text; walletRestoreFromKeysFormKey.currentState!.addressController.text;
credentials['spendKey'] = credentials['spendKey'] =
@ -519,7 +519,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
} }
}, },
onViewKeyEntered: (bool entered) { onViewKeyEntered: (bool entered) {
if (walletRestoreViewModel.type == WalletType.decred) { if (widget.walletRestoreViewModel.onlyViewKeyRestore) {
walletRestoreViewModel.isButtonEnabled = entered; walletRestoreViewModel.isButtonEnabled = entered;
} }
}, },
@ -536,7 +536,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
key: widget.walletRestoreFromSeedFormKey, key: widget.walletRestoreFromSeedFormKey,
restoredWallet: walletRestoreViewModel.restoredWallet, restoredWallet: walletRestoreViewModel.restoredWallet,
seedSettingsViewModel: widget.seedSettingsViewModel, seedSettingsViewModel: widget.seedSettingsViewModel,
displayBlockHeightSelector: widget.walletRestoreViewModel.hasBlockchainHeightLanguageSelector, displayBlockHeightSelector: widget.walletRestoreViewModel.hasBlockchainHeightSelector,
displayLanguageSelector: widget.walletRestoreViewModel.hasSeedLanguageSelector, displayLanguageSelector: widget.walletRestoreViewModel.hasSeedLanguageSelector,
type: widget.walletRestoreViewModel.type, type: widget.walletRestoreViewModel.type,
blockHeightFocusNode: widget.blockHeightFocusNode, blockHeightFocusNode: widget.blockHeightFocusNode,
@ -563,7 +563,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
} }
void _validateOnChange({bool isPolyseed = false}) { void _validateOnChange({bool isPolyseed = false}) {
if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightLanguageSelector) { if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightSelector) {
final hasHeight = walletRestoreFromSeedFormKey final hasHeight = walletRestoreFromSeedFormKey
.currentState?.blockchainHeightKey.currentState?.restoreHeightController.text.isNotEmpty; .currentState?.blockchainHeightKey.currentState?.restoreHeightController.text.isNotEmpty;

View file

@ -1,5 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:bbqrdart/bbqrdart.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/monero/monero.dart';
@ -7,31 +9,33 @@ import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/utils/clipboard_util.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/animated_ur_model.dart'; import 'package:cake_wallet/view_model/animated_ur_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// ur:xmr-txunsigned - unsigned transaction // ur:xmr-txunsigned - unsigned transaction
// should show a scanner afterwards. // should show a scanner afterwards.
class AnimatedURPage extends BasePage { class AnimatedURPage extends BasePage {
final bool isAll; final bool isAll;
AnimatedURPage(this.animatedURmodel, {required String urQr, this.isAll = false}) { AnimatedURPage(this.animatedURmodel, {
if (urQr == "export-outputs") { required this.urQr,
this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, false); this.isAll = false,
} else if (urQr == "export-outputs-all") { });
this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, true);
} else {
this.urQr = urQr;
}
}
late String urQr; late Map<String, String> urQr;
final AnimatedURModel animatedURmodel; final AnimatedURModel animatedURmodel;
String get urQrType { String get urQrType {
final first = urQr.trim().split("\n")[0]; if (urQr.values.first.trim().substring(0, 2) == BBQR.header) {
return BBQR.header;
}
if (urQr.isEmpty) return "unknown";
final first = urQr.values.first.trim().split("\n")[0];
return first.split('/')[0]; return first.split('/')[0];
} }
@ -43,11 +47,11 @@ class AnimatedURPage extends BasePage {
Padding( Padding(
padding: const EdgeInsets.only(top: 64.0), padding: const EdgeInsets.only(top: 64.0),
child: URQR( child: URQR(
frames: urQr.trim().split("\n"), urqr: urQr,
), ),
), ),
if (["ur:xmr-txunsigned", "ur:xmr-output", "ur:psbt", BBQR.header].contains(urQrType)) ...{
SizedBox(height: 32), SizedBox(height: 32),
if (urQrType == "ur:xmr-txunsigned" || urQrType == "ur:xmr-output")
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox( child: SizedBox(
@ -60,8 +64,11 @@ class AnimatedURPage extends BasePage {
), ),
), ),
), ),
SizedBox(height: 32), },
if (urQrType == "ur:xmr-output" && !isAll) Padding(
if (urQrType == "ur:xmr-output" && !isAll) ...{
SizedBox(height: 32),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox( child: SizedBox(
width: double.maxFinite, width: double.maxFinite,
@ -73,6 +80,7 @@ class AnimatedURPage extends BasePage {
), ),
), ),
), ),
},
], ],
); );
} }
@ -81,7 +89,13 @@ class AnimatedURPage extends BasePage {
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
return AnimatedURPage(animatedURmodel, urQr: "export-outputs-all", isAll: true); return AnimatedURPage(
animatedURmodel,
urQr: {
"export-outputs-all": "export-outputs-all",
},
isAll: true,
);
}, },
), ),
); );
@ -106,6 +120,11 @@ class AnimatedURPage extends BasePage {
Navigator.of(context).pop(true); Navigator.of(context).pop(true);
} }
break; break;
case "ur:psbt": // psbt
final ur = await presentQRScanner(context);
if (ur == null) return;
await bitcoin!.commitPsbtUR(animatedURmodel.wallet, ur.trim().split("\n"));
Navigator.of(context).pop(true);
default: default:
throw UnimplementedError("unable to handle UR: ${urQrType}"); throw UnimplementedError("unable to handle UR: ${urQrType}");
} }
@ -124,9 +143,9 @@ class AnimatedURPage extends BasePage {
} }
class URQR extends StatefulWidget { class URQR extends StatefulWidget {
URQR({super.key, required this.frames}); URQR({super.key, required this.urqr});
List<String> frames; final Map<String, String> urqr;
@override @override
// ignore: library_private_types_in_public_api // ignore: library_private_types_in_public_api
@ -160,6 +179,24 @@ class _URQRState extends State<URQR> {
super.dispose(); super.dispose();
} }
late String selected = (widget.urqr.isEmpty) ? "unknown" : widget.urqr.keys.first;
int selectedInt = 0;
List<String> get frames {
return widget.urqr[selected]?.split("\n") ?? [];
}
late String nextLabel = widget.urqr.keys.toList()[(selectedInt + 1) % widget.urqr.length] ;
void next() {
final keys = widget.urqr.keys.toList();
selectedInt++;
setState(() {
nextLabel = keys[(selectedInt) % keys.length];
selected = keys[(selectedInt) % keys.length];
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
@ -168,10 +205,37 @@ class _URQRState extends State<URQR> {
children: [ children: [
Center( Center(
child: QrImage( child: QrImage(
data: widget.frames[frame % widget.frames.length], version: -1, data: frames[frame % frames.length],
version: -1,
size: 400, size: 400,
), ),
), ),
if (widget.urqr.values.length > 1)
SizedBox(
width: double.maxFinite,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: double.maxFinite,
child: PrimaryButton(
onPressed: next,
text: nextLabel,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
),
),
if (FeatureFlag.hasDevOptions) ...{
TextButton(
onPressed: () {
Clipboard.setData(ClipboardData(text: """Current frame (${frame % frames.length}): ${frames[frame % frames.length]},
All frames:
- ${frames.join("\n - ")}"""));
},
child: Text(frames[frame % frames.length]),
),
}
], ],
); );
} }

View file

@ -82,7 +82,7 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
showLegacySeedTab = widget.walletKeysViewModel.legacySeedSplit.isNotEmpty; showLegacySeedTab = widget.walletKeysViewModel.legacySeedSplit.isNotEmpty;
isLegacySeedOnly = widget.walletKeysViewModel.isLegacySeedOnly; isLegacySeedOnly = widget.walletKeysViewModel.isLegacySeedOnly;
final totalTabs = 1 + (showKeyTab ? 1 : 0) + (showLegacySeedTab ? 1 : 0); final totalTabs = (_hasSeeds ? 1 : 0) + (showKeyTab ? 1 : 0) + (showLegacySeedTab ? 1 : 0);
_tabController = TabController(length: totalTabs, vsync: this); _tabController = TabController(length: totalTabs, vsync: this);
} }
@ -126,7 +126,7 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
dividerColor: Colors.transparent, dividerColor: Colors.transparent,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
tabs: [ tabs: [
Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')), if (_hasSeeds) Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')),
if (showKeyTab) Tab(text: S.of(context).keys, key: ValueKey('wallet_keys_page_keys'),), if (showKeyTab) Tab(text: S.of(context).keys, key: ValueKey('wallet_keys_page_keys'),),
if (showLegacySeedTab) Tab(text: S.of(context).legacy, key: ValueKey('wallet_keys_page_seed_legacy')), if (showLegacySeedTab) Tab(text: S.of(context).legacy, key: ValueKey('wallet_keys_page_seed_legacy')),
], ],
@ -137,10 +137,11 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
child: TabBarView( child: TabBarView(
controller: _tabController, controller: _tabController,
children: [ children: [
Padding( if (_hasSeeds)
padding: const EdgeInsets.only(left: 22, right: 22), Padding(
child: _buildSeedTab(context, false), padding: const EdgeInsets.only(left: 22, right: 22),
), child: _buildSeedTab(context, false),
),
if (showKeyTab) if (showKeyTab)
Padding( Padding(
padding: const EdgeInsets.only(left: 22, right: 22), padding: const EdgeInsets.only(left: 22, right: 22),

View file

@ -22,7 +22,7 @@ abstract class ReceiveOptionViewModelBase with Store {
switch (walletType) { switch (walletType) {
case WalletType.bitcoin: case WalletType.bitcoin:
_options = [ _options = [
...bitcoin!.getBitcoinReceivePageOptions(), ...bitcoin!.getBitcoinReceivePageOptions(_wallet),
...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet)
]; ];
break; break;

View file

@ -35,7 +35,7 @@ class RestoredWallet {
factory RestoredWallet.fromKey(Map<String, dynamic> json) { factory RestoredWallet.fromKey(Map<String, dynamic> json) {
try { try {
final codeParsed = jsonDecode(json['raw_qr'].toString()); final codeParsed = jsonDecode(json['raw_qr'].toString());
if (codeParsed["version"] == 0) { if (codeParsed["version"] == 0) {
json['address'] = codeParsed["primaryAddress"]; json['address'] = codeParsed["primaryAddress"];
json['view_key'] = codeParsed["privateViewKey"]; json['view_key'] = codeParsed["privateViewKey"];
@ -44,6 +44,7 @@ class RestoredWallet {
} catch (e) { } catch (e) {
// fine, we don't care, it is only for monero anyway // fine, we don't care, it is only for monero anyway
} }
json['view_key'] ??= json['xpub'];
final height = json['height'] as String?; final height = json['height'] as String?;
return RestoredWallet( return RestoredWallet(
restoreMode: json['mode'] as WalletRestoreMode, restoreMode: json['mode'] as WalletRestoreMode,
@ -51,7 +52,7 @@ class RestoredWallet {
address: json['address'] as String?, address: json['address'] as String?,
spendKey: json['spend_key'] as String?, spendKey: json['spend_key'] as String?,
viewKey: json['view_key'] as String?, viewKey: json['view_key'] as String?,
height: height != null ? int.tryParse(height)??0 : 0, height: height != null ? int.tryParse(height) ?? 0 : 0,
privateKey: json['private_key'] as String?, privateKey: json['private_key'] as String?,
); );
} }

View file

@ -54,6 +54,8 @@ class WalletRestoreFromQRCode {
final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key)); final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key));
if (code.startsWith("xpub")) return WalletType.bitcoin;
if (extracted == null) { if (extracted == null) {
// Special case for view-only monero wallet // Special case for view-only monero wallet
try { try {
@ -117,11 +119,15 @@ class WalletRestoreFromQRCode {
formattedUri = seedPhrase != null formattedUri = seedPhrase != null
? '$walletType:?seed=$seedPhrase' ? '$walletType:?seed=$seedPhrase'
: throw Exception('Failed to determine valid seed phrase'); : code.startsWith('xpub')
? '$walletType:?xpub=$code'
: throw Exception('Failed to determine valid seed phrase');
} else { } else {
final index = code.indexOf(':'); final index = code.indexOf(':');
final query = code.substring(index + 1).replaceAll('?', '&'); final query = code.substring(index + 1).replaceAll('?', '&');
formattedUri = '$walletType:?$query'; formattedUri = code.startsWith('xpub')
? '$walletType:?xpub=$code'
:'$walletType:?$query';
} }
final uri = Uri.parse(formattedUri); final uri = Uri.parse(formattedUri);
@ -158,6 +164,10 @@ class WalletRestoreFromQRCode {
throw Exception('Unexpected restore mode: tx_payment_id is invalid'); throw Exception('Unexpected restore mode: tx_payment_id is invalid');
} }
if (credentials.containsKey("xpub")) {
return WalletRestoreMode.keys;
}
if (credentials['seed'] != null) { if (credentials['seed'] != null) {
final seedValue = credentials['seed'] as String; final seedValue = credentials['seed'] as String;
final words = SeedValidator.getWordList(type: type, language: 'english'); final words = SeedValidator.getWordList(type: type, language: 'english');

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
@ -162,18 +163,21 @@ abstract class WalletKeysViewModelBase with Store {
case WalletType.bitcoin: case WalletType.bitcoin:
case WalletType.litecoin: case WalletType.litecoin:
case WalletType.bitcoinCash: case WalletType.bitcoinCash:
final keys = bitcoin!.getWalletKeys(_appStore.wallet!);
items.addAll([
if ((keys['wif']??'').isNotEmpty)
StandartListItem(title: "WIF", value: keys['wif']!),
if ((keys['privateKey']??'').isNotEmpty)
StandartListItem(title: S.current.private_key, value: keys['privateKey']!),
if (keys['publicKey'] != null)
StandartListItem(title: S.current.public_key, value: keys['publicKey']!),
if (keys['xpub'] != null)
StandartListItem(title: "xPub", value: keys['xpub']!),
]);
break;
case WalletType.none: case WalletType.none:
case WalletType.haven: case WalletType.haven:
// final keys = bitcoin!.getWalletKeys(_appStore.wallet!);
//
// items.addAll([
// if (keys['wif'] != null)
// StandartListItem(title: "WIF", value: keys['wif']!),
// if (keys['privateKey'] != null)
// StandartListItem(title: S.current.private_key, value: keys['privateKey']!),
// if (keys['publicKey'] != null)
// StandartListItem(title: S.current.public_key, value: keys['publicKey']!),
// ]);
break; break;
} }

View file

@ -32,17 +32,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
WalletRestoreViewModelBase(AppStore appStore, WalletCreationService walletCreationService, WalletRestoreViewModelBase(AppStore appStore, WalletCreationService walletCreationService,
Box<WalletInfo> walletInfoSource, SeedSettingsViewModel seedSettingsViewModel, Box<WalletInfo> walletInfoSource, SeedSettingsViewModel seedSettingsViewModel,
{required WalletType type, this.restoredWallet}) {required WalletType type, this.restoredWallet})
: hasSeedLanguageSelector = : isButtonEnabled = restoredWallet != null,
type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero,
hasBlockchainHeightLanguageSelector =
type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero,
hasRestoreFromPrivateKey = type == WalletType.ethereum ||
type == WalletType.polygon ||
type == WalletType.nano ||
type == WalletType.banano ||
type == WalletType.solana ||
type == WalletType.tron,
isButtonEnabled = false,
hasPassphrase = false, hasPassphrase = false,
mode = restoredWallet?.restoreMode ?? WalletRestoreMode.seed, mode = restoredWallet?.restoreMode ?? WalletRestoreMode.seed,
super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel,
@ -60,9 +50,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
case WalletType.ethereum: case WalletType.ethereum:
case WalletType.polygon: case WalletType.polygon:
case WalletType.decred: case WalletType.decred:
case WalletType.bitcoin:
availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys];
break; break;
case WalletType.bitcoin:
case WalletType.litecoin: case WalletType.litecoin:
case WalletType.bitcoinCash: case WalletType.bitcoinCash:
case WalletType.zano: case WalletType.zano:
@ -82,9 +72,34 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
static const decredSeedMnemonicLength = 15; static const decredSeedMnemonicLength = 15;
late List<WalletRestoreMode> availableModes; late List<WalletRestoreMode> availableModes;
final bool hasSeedLanguageSelector; late final bool hasSeedLanguageSelector = [
final bool hasBlockchainHeightLanguageSelector; WalletType.monero,
final bool hasRestoreFromPrivateKey; WalletType.haven,
WalletType.wownero
].contains(type);
late final bool hasBlockchainHeightSelector = [
WalletType.monero,
WalletType.haven,
WalletType.wownero
].contains(type);
late final bool hasRestoreFromPrivateKey = [
WalletType.ethereum,
WalletType.polygon,
WalletType.nano,
WalletType.banano,
WalletType.solana,
WalletType.tron
].contains(type);
late final bool onlyViewKeyRestore = [
WalletType.bitcoin,
WalletType.litecoin,
WalletType.bitcoinCash,
WalletType.decred
].contains(type);
final RestoredWallet? restoredWallet; final RestoredWallet? restoredWallet;
@observable @observable
@ -198,6 +213,13 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
final address = options['address'] as String?; final address = options['address'] as String?;
switch (type) { switch (type) {
case WalletType.bitcoin:
return bitcoin!.createBitcoinWalletFromKeys(
name: name,
password: password,
xpub: viewKey!,
);
case WalletType.monero: case WalletType.monero:
return monero!.createMoneroRestoreWalletFromKeysCredentials( return monero!.createMoneroRestoreWalletFromKeysCredentials(
name: name, name: name,
@ -276,8 +298,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
case WalletType.litecoin: case WalletType.litecoin:
String? mnemonic = credentials['seed'] as String?; String? mnemonic = credentials['seed'] as String?;
String? passphrase = credentials['passphrase'] as String?; String? passphrase = credentials['passphrase'] as String?;
if (mnemonic == null) break;
return bitcoin!.getDerivationsFromMnemonic( return bitcoin!.getDerivationsFromMnemonic(
mnemonic: mnemonic!, mnemonic: mnemonic,
node: node, node: node,
passphrase: passphrase, passphrase: passphrase,
); );

View file

@ -130,6 +130,14 @@ dependencies:
url: https://github.com/MrCyjaneK/flutter_daemon url: https://github.com/MrCyjaneK/flutter_daemon
ref: c24ee99f2f3070ea02d8108bbdd7727d73f7e5f1 ref: c24ee99f2f3070ea02d8108bbdd7727d73f7e5f1
flutter_local_notifications: ^19.0.0 flutter_local_notifications: ^19.0.0
ur:
git:
url: https://github.com/bukata-sa/bc-ur-dart
ref: 5738f70d0ec3d50977ac3dd01fed62939600238b
bbqrdart:
git:
url: https://github.com/mrcyjanek/bbqrdart
ref: b0f3f72911221bafed0ab77359fccd64bc79d524
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -14,7 +14,7 @@ cd "$SCRIPT_DIR"
BUILD_AMD64=false BUILD_AMD64=false
BUILD_ARM64=false BUILD_ARM64=false
APP_TYPE="cakewallet" APP_TYPE="cakewallet"
DOCKER_IMAGE="ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly" DOCKER_IMAGE="ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly"
# Parse arguments # Parse arguments
for arg in "$@" for arg in "$@"

View file

@ -4,7 +4,7 @@
FROM mcr.microsoft.com/windows/servercore:ltsc2022 FROM mcr.microsoft.com/windows/servercore:ltsc2022
ENV FLUTTER_VERSION=3.27.0 ENV FLUTTER_VERSION=3.27.4
ENV GIT_VERSION=2.47.1 ENV GIT_VERSION=2.47.1
ENV VS_INSTALLED_DIR="C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools" ENV VS_INSTALLED_DIR="C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools"
ENV PATH="C:\Users\ContainerAdministrator\.cargo\bin;C:\ProgramData\chocolatey\bin;C:\flutter\flutter\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Users\ContainerAdministrator\AppData\Local\Microsoft\WindowsApps" ENV PATH="C:\Users\ContainerAdministrator\.cargo\bin;C:\ProgramData\chocolatey\bin;C:\flutter\flutter\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Users\ContainerAdministrator\AppData\Local\Microsoft\WindowsApps"

View file

@ -163,6 +163,7 @@ abstract class Bitcoin {
String? passphrase, String? passphrase,
}); });
WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({required String name, required String password, required String wif, WalletInfo? walletInfo}); WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({required String name, required String password, required String wif, WalletInfo? walletInfo});
WalletCredentials createBitcoinWalletFromKeys({required String name, required String password, required String xpub});
WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? passphrase, String? mnemonic}); WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? passphrase, String? mnemonic});
WalletCredentials createBitcoinHardwareWalletCredentials({required String name, required HardwareAccountData accountData, WalletInfo? walletInfo}); WalletCredentials createBitcoinHardwareWalletCredentials({required String name, required HardwareAccountData accountData, WalletInfo? walletInfo});
List<String> getWordList(); List<String> getWordList();
@ -206,7 +207,7 @@ abstract class Bitcoin {
Map<DerivationType, List<DerivationInfo>> getElectrumDerivations(); Map<DerivationType, List<DerivationInfo>> getElectrumDerivations();
Future<void> setAddressType(Object wallet, dynamic option); Future<void> setAddressType(Object wallet, dynamic option);
ReceivePageOption getSelectedAddressType(Object wallet); ReceivePageOption getSelectedAddressType(Object wallet);
List<ReceivePageOption> getBitcoinReceivePageOptions(); List<ReceivePageOption> getBitcoinReceivePageOptions(Object wallet);
List<ReceivePageOption> getLitecoinReceivePageOptions(); List<ReceivePageOption> getLitecoinReceivePageOptions();
BitcoinAddressType getBitcoinAddressType(ReceivePageOption option); BitcoinAddressType getBitcoinAddressType(ReceivePageOption option);
bool isPayjoinAvailable(Object wallet); bool isPayjoinAvailable(Object wallet);
@ -244,6 +245,7 @@ abstract class Bitcoin {
bool getMwebEnabled(Object wallet); bool getMwebEnabled(Object wallet);
String? getUnusedMwebAddress(Object wallet); String? getUnusedMwebAddress(Object wallet);
String? getUnusedSegwitAddress(Object wallet); String? getUnusedSegwitAddress(Object wallet);
Future<void> commitPsbtUR(Object wallet, List<String> urCodes);
void updatePayjoinState(Object wallet, bool state); void updatePayjoinState(Object wallet, bool state);
String getPayjoinEndpoint(Object wallet); String getPayjoinEndpoint(Object wallet);