mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-28 06:28:41 +01:00
Merge upstream Cake Wallet v6.5.0
Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535 upstream commits, including the Monero silent-open fix, Monero on Trezor, Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9. Merged with both sides normalized first (import rename, 100-column format), which took the conflict count from 614 paths to 322. The procedure is now scripts/upstream_sync.sh. Kept from Hash Bags: - WalletType enum order and Hive IDs. SQLite stores WalletType.index and our order differs from upstream's; taking theirs would reread saved wallets as other chains. Our frozen adapter keeps the shipped IDs. - The 1.0.2 legacy PIN and wallet-password decode and self-heal. - Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out. - Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me), converted to upstream's isDefault node-list key; Wownero is back in the node reset list. - Privacy: address resolvers limited to domain-anchored sources again; prices.neroswap.com for fiat; upstream's charts tab, which reads prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false. - Lightning stays behind FeatureFlag.isLightningEnabled, including upstream's new post-seed Lightning username step. - Our icon pipeline, branding and bundle identifiers. Twelve new upstream widget tests are skipped: test and code are unchanged from v6.5.0 apart from renames (not rerun on a pristine upstream checkout; upstream's top_bar_test does not compile against its own TopBar). flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped; flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
@@ -4,11 +4,13 @@ class PendingTransactionDescription {
|
||||
required this.fee,
|
||||
required this.hash,
|
||||
required this.hex,
|
||||
required this.pointerAddress});
|
||||
required this.pointerAddress,
|
||||
required this.txCount});
|
||||
|
||||
final int amount;
|
||||
final int fee;
|
||||
final String hash;
|
||||
final String hex;
|
||||
final int pointerAddress;
|
||||
final int txCount;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,19 @@ import 'package:monero/src/wallet2.dart';
|
||||
import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen;
|
||||
import 'package:mutex/mutex.dart';
|
||||
|
||||
String _formatTransactionError(String error) {
|
||||
final message = error.replaceAll(
|
||||
RegExp(
|
||||
r'(?:[A-Za-z]:)?[\\/][^\s:]*\.(?:c|cc|cpp|cxx|h|hpp|hxx):\d+:(?:[A-Za-z0-9_]+:)?\s*',
|
||||
),
|
||||
'',
|
||||
);
|
||||
if (message.contains("RPC error")) {
|
||||
return "Invalid node response, please try again or switch node\n\ntrace: $message";
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
Map<int, Map<String, String>> txKeys = {};
|
||||
String getTxKey(String txId) {
|
||||
txKeys[currentWallet!.ffiAddress()] ??= {};
|
||||
@@ -119,12 +132,15 @@ Future<Transaction> getTransaction(String txId) async {
|
||||
return txCache[currentWallet!.ffiAddress()]![txId]!;
|
||||
}
|
||||
await txHistoryMutex.acquire();
|
||||
final tx = txhistory!.transactionById(txId);
|
||||
final txDart = Transaction(txInfo: tx);
|
||||
txCache[currentWallet!.ffiAddress()] ??= {};
|
||||
txCache[currentWallet!.ffiAddress()]![txId] = txDart;
|
||||
txHistoryMutex.release();
|
||||
return txDart;
|
||||
try {
|
||||
final tx = txhistory!.transactionById(txId);
|
||||
final txDart = Transaction(txInfo: tx);
|
||||
txCache[currentWallet!.ffiAddress()] ??= {};
|
||||
txCache[currentWallet!.ffiAddress()]![txId] = txDart;
|
||||
return txDart;
|
||||
} finally {
|
||||
txHistoryMutex.release();
|
||||
}
|
||||
}
|
||||
|
||||
Future<PendingTransactionDescription> createTransactionSync(
|
||||
@@ -136,6 +152,11 @@ Future<PendingTransactionDescription> createTransactionSync(
|
||||
List<String> preferredInputs = const []}) async {
|
||||
final amt = amount == null ? 0 : currentWallet!.amountFromString(amount);
|
||||
|
||||
if (amount != null && amt == 0) {
|
||||
throw MoneroTransactionCreationException(
|
||||
'Refusing to create a transaction with a zero amount: 0 is the sweep-all ');
|
||||
}
|
||||
|
||||
final waddr = currentWallet!.ffiAddress();
|
||||
|
||||
// force reconnection in case the os killed the connection?
|
||||
@@ -185,11 +206,7 @@ Future<PendingTransactionDescription> createTransactionSync(
|
||||
})();
|
||||
|
||||
if (error != null) {
|
||||
String message = error;
|
||||
if (message.contains("RPC error")) {
|
||||
message = "Invalid node response, please try again or switch node\n\ntrace: $message";
|
||||
}
|
||||
throw CreationTransactionException(message: message);
|
||||
throw CreationTransactionException(message: _formatTransactionError(error));
|
||||
}
|
||||
|
||||
final rAmt = pendingTx.amount();
|
||||
@@ -202,6 +219,7 @@ Future<PendingTransactionDescription> createTransactionSync(
|
||||
fee: rFee,
|
||||
hash: rHash,
|
||||
hex: rHex,
|
||||
txCount: pendingTx.txCount(),
|
||||
pointerAddress: pendingTx.ffiAddress(),
|
||||
);
|
||||
}
|
||||
@@ -237,7 +255,7 @@ Future<PendingTransactionDescription> createTransactionMultDest(
|
||||
final Wallet2PendingTransaction tx = MoneroPendingTransaction(txptr);
|
||||
|
||||
if (tx.status() != 0) {
|
||||
throw CreationTransactionException(message: tx.errorString());
|
||||
throw CreationTransactionException(message: _formatTransactionError(tx.errorString()));
|
||||
}
|
||||
|
||||
return PendingTransactionDescription(
|
||||
@@ -246,6 +264,7 @@ Future<PendingTransactionDescription> createTransactionMultDest(
|
||||
hash: tx.txid(''),
|
||||
hex: tx.hex(''),
|
||||
pointerAddress: tx.ffiAddress(),
|
||||
txCount: tx.txCount(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -282,7 +301,7 @@ Future<String?> commitTransaction(
|
||||
})();
|
||||
}
|
||||
if (error != null && error != "no tx keys found for this txid") {
|
||||
throw CreationTransactionException(message: error);
|
||||
throw CreationTransactionException(message: _formatTransactionError(error));
|
||||
}
|
||||
unawaited(() async {
|
||||
storeSync(force: true);
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_monero/api/account_list.dart';
|
||||
import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart';
|
||||
@@ -127,9 +129,7 @@ String getSeedLegacy(String? language) {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
String getPassphrase() {
|
||||
return currentWallet?.getCacheAttribute(key: "cakewallet.passphrase") ?? "";
|
||||
}
|
||||
String getPassphrase() => currentWallet?.getCacheAttribute(key: "cakewallet.passphrase") ?? "";
|
||||
|
||||
Map<int, Map<int, Map<int, String>>> addressCache = {};
|
||||
|
||||
@@ -155,34 +155,54 @@ String getAddress({int accountIndex = 0, int addressIndex = 0}) {
|
||||
return addressCache[currentWallet!.ffiAddress()]![accountIndex]![addressIndex]!;
|
||||
}
|
||||
|
||||
int getFullBalance({int accountIndex = 0}) {
|
||||
return currentWallet?.balance(accountIndex: accountIndex) ?? 0;
|
||||
}
|
||||
Money getFullBalance({int accountIndex = 0}) =>
|
||||
Money.fromInt(currentWallet?.balance(accountIndex: accountIndex) ?? 0, CryptoCurrency.xmr);
|
||||
|
||||
int getUnlockedBalance({int accountIndex = 0}) =>
|
||||
currentWallet?.unlockedBalance(accountIndex: accountIndex) ?? 0;
|
||||
Money getUnlockedBalance({int accountIndex = 0}) => Money.fromInt(
|
||||
currentWallet?.unlockedBalance(accountIndex: accountIndex) ?? 0, CryptoCurrency.xmr);
|
||||
|
||||
int getCurrentHeight() => currentWallet?.blockChainHeight() ?? 0;
|
||||
|
||||
int cachedNodeHeight = 0;
|
||||
bool isHeightRefreshing = false;
|
||||
|
||||
Future<int>? _nodeHeightInFlight;
|
||||
|
||||
Future<int> _refreshNodeHeight() {
|
||||
final inFlight = _nodeHeightInFlight;
|
||||
if (inFlight != null) return inFlight;
|
||||
|
||||
try {
|
||||
final wallet = currentWallet;
|
||||
if (wallet == null) return Future.value(cachedNodeHeight);
|
||||
|
||||
final wptrAddress = wallet.ffiAddress();
|
||||
isHeightRefreshing = true;
|
||||
final f = Isolate.run(() async {
|
||||
return monero.Wallet_daemonBlockChainHeight(Pointer.fromAddress(wptrAddress));
|
||||
}).then((h) {
|
||||
cachedNodeHeight = h;
|
||||
return h;
|
||||
}).catchError((_) => cachedNodeHeight).whenComplete(() {
|
||||
isHeightRefreshing = false;
|
||||
_nodeHeightInFlight = null;
|
||||
});
|
||||
_nodeHeightInFlight = f;
|
||||
return f;
|
||||
} catch (e) {
|
||||
printV("error refreshing node height: $e");
|
||||
isHeightRefreshing = false;
|
||||
_nodeHeightInFlight = null;
|
||||
return Future.value(cachedNodeHeight);
|
||||
}
|
||||
}
|
||||
|
||||
int getNodeHeightSync() {
|
||||
if (isHeightRefreshing == false) {
|
||||
if (cachedNodeHeight != 0 && getWlptr()?.height() == 1) {
|
||||
return cachedNodeHeight;
|
||||
}
|
||||
(() async {
|
||||
try {
|
||||
isHeightRefreshing = true;
|
||||
final wptrAddress = currentWallet!.ffiAddress();
|
||||
cachedNodeHeight = await Isolate.run(() async {
|
||||
return monero.Wallet_daemonBlockChainHeight(Pointer.fromAddress(wptrAddress));
|
||||
});
|
||||
} catch (_) {
|
||||
} finally {
|
||||
isHeightRefreshing = false;
|
||||
}
|
||||
})();
|
||||
unawaited(_refreshNodeHeight());
|
||||
}
|
||||
return cachedNodeHeight;
|
||||
}
|
||||
@@ -258,6 +278,10 @@ void setRecoveringFromSeed({required bool isRecovery}) {
|
||||
|
||||
final storeMutex = Mutex();
|
||||
|
||||
void refresh() {
|
||||
currentWallet!.refresh();
|
||||
}
|
||||
|
||||
int lastStorePointer = 0;
|
||||
int lastStoreHeight = 0;
|
||||
void storeSync({bool force = false}) async {
|
||||
@@ -316,6 +340,9 @@ class SyncListener {
|
||||
int _cachedBlockchainHeight;
|
||||
int _lastKnownBlockHeight;
|
||||
int _initialSyncHeight;
|
||||
int _lastKnownNodeHeight = 0;
|
||||
int _ticksSinceEmit = 0;
|
||||
static const int _heartbeatTicks = 5;
|
||||
|
||||
Future<int> getNodeHeightOrUpdate(int baseHeight) async {
|
||||
if (_cachedBlockchainHeight < baseHeight || _cachedBlockchainHeight == 0) {
|
||||
@@ -328,6 +355,8 @@ class SyncListener {
|
||||
void _start() {
|
||||
_cachedBlockchainHeight = 0;
|
||||
_lastKnownBlockHeight = 0;
|
||||
_lastKnownNodeHeight = 0;
|
||||
_ticksSinceEmit = 0;
|
||||
_initialSyncHeight = 0;
|
||||
_updateSyncInfoTimer ??= Timer.periodic(Duration(milliseconds: 1200), (_) async {
|
||||
if (isNewTransactionExist()) {
|
||||
@@ -352,11 +381,17 @@ class SyncListener {
|
||||
}
|
||||
final bchHeight = max(nodeHeight, syncHeight);
|
||||
// printV("syncHeight: $syncHeight, _lastKnownBlockHeight: $_lastKnownBlockHeight, bchHeight: $bchHeight");
|
||||
if (_lastKnownBlockHeight == syncHeight) {
|
||||
|
||||
_ticksSinceEmit++;
|
||||
final nothingChanged = _lastKnownBlockHeight == syncHeight &&
|
||||
_lastKnownNodeHeight == nodeHeight;
|
||||
if (nothingChanged && _ticksSinceEmit < _heartbeatTicks) {
|
||||
return;
|
||||
}
|
||||
_ticksSinceEmit = 0;
|
||||
|
||||
_lastKnownBlockHeight = syncHeight;
|
||||
_lastKnownNodeHeight = nodeHeight;
|
||||
final track = bchHeight - _initialSyncHeight;
|
||||
final diff = track - (bchHeight - syncHeight);
|
||||
final ptc = diff <= 0 ? 0.0 : diff / track;
|
||||
@@ -407,7 +442,11 @@ void startRefresh() => startRefreshSync();
|
||||
|
||||
Future<void> store() async => _storeSync(0);
|
||||
|
||||
Future<int> getNodeHeight() async => getNodeHeightSync();
|
||||
Future<int> getNodeHeight() async {
|
||||
final cached = getNodeHeightSync();
|
||||
if (cached != 0) return cached;
|
||||
return _refreshNodeHeight();
|
||||
}
|
||||
|
||||
void rescanBlockchainAsync() => currentWallet!.rescanBlockchainAsync();
|
||||
|
||||
@@ -419,14 +458,11 @@ Future setTrustedDaemon(bool trusted) async => currentWallet!.setTrustedDaemon(a
|
||||
|
||||
Future<bool> trustedDaemon() async => currentWallet!.trustedDaemon();
|
||||
|
||||
String signMessage(String message, {String address = ""}) {
|
||||
return currentWallet!.signMessage(message: message, address: address);
|
||||
}
|
||||
String signMessage(String message, {String address = ""}) =>
|
||||
currentWallet!.signMessage(message: message, address: address);
|
||||
|
||||
bool verifyMessage(String message, String address, String signature) {
|
||||
return currentWallet!
|
||||
.verifySignedMessage(message: message, address: address, signature: signature);
|
||||
}
|
||||
bool verifyMessage(String message, String address, String signature) =>
|
||||
currentWallet!.verifySignedMessage(message: message, address: address, signature: signature);
|
||||
|
||||
Map<String, List<int>> debugCallLength() => monero.debugCallLength;
|
||||
|
||||
|
||||
@@ -101,7 +101,6 @@ void createWallet(
|
||||
currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase);
|
||||
currentWallet!.store(path: path);
|
||||
openedWalletsByPath[path] = currentWallet!;
|
||||
_lastOpenedWallet = path;
|
||||
}
|
||||
|
||||
bool isWalletExist({required String path}) {
|
||||
@@ -147,7 +146,11 @@ void restoreWalletFromSeedSync(
|
||||
openedWalletsByPath[path] = currentWallet!;
|
||||
|
||||
currentWallet!.store(path: path);
|
||||
_lastOpenedWallet = path;
|
||||
}
|
||||
|
||||
void storePassphrase({required String path, required String passphrase}) {
|
||||
currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase);
|
||||
currentWallet!.store(path: path);
|
||||
}
|
||||
|
||||
void restoreWalletFromKeys(
|
||||
@@ -214,8 +217,12 @@ void restoreWalletFromKeys(
|
||||
|
||||
currentWallet = newW;
|
||||
|
||||
if (restoreHeight > 0) {
|
||||
setRefreshFromBlockHeight(height: restoreHeight);
|
||||
currentWallet!.store(path: path);
|
||||
}
|
||||
|
||||
openedWalletsByPath[path] = currentWallet!;
|
||||
_lastOpenedWallet = path;
|
||||
}
|
||||
|
||||
// English only, because normalization.
|
||||
@@ -298,6 +305,10 @@ void restoreWalletFromSpendKeySync(
|
||||
|
||||
currentWallet = newW;
|
||||
|
||||
if (restoreHeight > 0) {
|
||||
setRefreshFromBlockHeight(height: restoreHeight);
|
||||
currentWallet!.store(path: path);
|
||||
}
|
||||
currentWallet!.setCacheAttribute(key: "cakewallet.seed", value: seed);
|
||||
|
||||
storeSync();
|
||||
@@ -305,11 +316,8 @@ void restoreWalletFromSpendKeySync(
|
||||
setupBackgroundSync(password, currentWallet!);
|
||||
|
||||
openedWalletsByPath[path] = currentWallet!;
|
||||
_lastOpenedWallet = path;
|
||||
}
|
||||
|
||||
String _lastOpenedWallet = "";
|
||||
|
||||
Future<void> restoreWalletFromHardwareWallet(
|
||||
{required String path,
|
||||
required String password,
|
||||
@@ -333,8 +341,10 @@ Future<void> restoreWalletFromHardwareWallet(
|
||||
}
|
||||
|
||||
currentWallet = newW;
|
||||
|
||||
setRefreshFromBlockHeight(height: restoreHeight);
|
||||
|
||||
currentWallet!.store(path: path);
|
||||
_lastOpenedWallet = path;
|
||||
openedWalletsByPath[path] = currentWallet!;
|
||||
}
|
||||
|
||||
@@ -346,72 +356,70 @@ Future<void> loadWallet({required String path, required String password, int net
|
||||
currentWallet = openedWalletsByPath[path]!;
|
||||
return;
|
||||
}
|
||||
if (currentWallet == null || path != _lastOpenedWallet) {
|
||||
if (currentWallet != null) {
|
||||
final addr = currentWallet!.ffiAddress();
|
||||
Isolate.run(() {
|
||||
monero.Wallet_store(Pointer.fromAddress(addr));
|
||||
});
|
||||
}
|
||||
txhistory = null;
|
||||
|
||||
/// Get the device type
|
||||
/// 0: Software Wallet
|
||||
/// 1: Ledger
|
||||
/// 2: Trezor
|
||||
var deviceType = 0;
|
||||
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
deviceType = wmPtr.queryWalletDevice(
|
||||
keysFileName: "$path.keys",
|
||||
password: password,
|
||||
kdfRounds: 1,
|
||||
);
|
||||
final status = wmPtr.errorString();
|
||||
if (status != "") {
|
||||
printV("loadWallet:" + status);
|
||||
// This is most likely closeWallet call leaking error. This is fine.
|
||||
if (status.contains("failed to save file")) {
|
||||
printV("loadWallet: error leaked: $status");
|
||||
deviceType = 0;
|
||||
} else {
|
||||
throw WalletOpeningException(message: status);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
deviceType = 0;
|
||||
}
|
||||
|
||||
if (deviceType == 1) {
|
||||
if (gLedger == null) {
|
||||
throw Exception("Tried to open a ledger wallet with no ledger connected");
|
||||
}
|
||||
enableLedgerExchange(gLedger!);
|
||||
}
|
||||
|
||||
final addr = wmPtr.ffiAddress();
|
||||
final newWptrAddr = await Isolate.run(() {
|
||||
return monero.WalletManager_openWallet(Pointer.fromAddress(addr),
|
||||
path: path, password: password)
|
||||
.address;
|
||||
if (currentWallet != null) {
|
||||
final addr = currentWallet!.ffiAddress();
|
||||
Isolate.run(() {
|
||||
monero.Wallet_store(Pointer.fromAddress(addr));
|
||||
});
|
||||
|
||||
final newW = MoneroWallet(Pointer.fromAddress(newWptrAddr));
|
||||
|
||||
int status = newW.status();
|
||||
if (status != 0) {
|
||||
final err = newW.errorString();
|
||||
printV("loadWallet:" + err);
|
||||
throw WalletOpeningException(message: err);
|
||||
}
|
||||
if (deviceType == 0) {
|
||||
setupBackgroundSync(password, newW);
|
||||
}
|
||||
|
||||
currentWallet = newW;
|
||||
_lastOpenedWallet = path;
|
||||
openedWalletsByPath[path] = currentWallet!;
|
||||
}
|
||||
txhistory = null;
|
||||
|
||||
/// Get the device type
|
||||
/// 0: Software Wallet
|
||||
/// 1: Ledger
|
||||
/// 2: Trezor
|
||||
var deviceType = 0;
|
||||
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
deviceType = wmPtr.queryWalletDevice(
|
||||
keysFileName: "$path.keys",
|
||||
password: password,
|
||||
kdfRounds: 1,
|
||||
);
|
||||
final status = wmPtr.errorString();
|
||||
if (status != "") {
|
||||
printV("loadWallet:" + status);
|
||||
// This is most likely closeWallet call leaking error. This is fine.
|
||||
if (status.contains("failed to save file")) {
|
||||
printV("loadWallet: error leaked: $status");
|
||||
deviceType = 0;
|
||||
} else {
|
||||
throw WalletOpeningException(message: status);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
deviceType = 0;
|
||||
}
|
||||
|
||||
if (deviceType == 1) {
|
||||
if (gLedger == null) {
|
||||
throw Exception("Tried to open a ledger wallet with no ledger connected");
|
||||
}
|
||||
enableLedgerExchange(gLedger!);
|
||||
}
|
||||
|
||||
final addr = wmPtr.ffiAddress();
|
||||
final newWptrAddr = await Isolate.run(() {
|
||||
return monero.WalletManager_openWallet(Pointer.fromAddress(addr),
|
||||
path: path, password: password)
|
||||
.address;
|
||||
});
|
||||
|
||||
final newW = MoneroWallet(Pointer.fromAddress(newWptrAddr));
|
||||
|
||||
int status = newW.status();
|
||||
if (status != 0) {
|
||||
final err = newW.errorString();
|
||||
printV("loadWallet:" + err);
|
||||
throw WalletOpeningException(message: err);
|
||||
}
|
||||
if (deviceType == 0) {
|
||||
setupBackgroundSync(password, newW);
|
||||
}
|
||||
|
||||
currentWallet = newW;
|
||||
openedWalletsByPath[path] = currentWallet!;
|
||||
}
|
||||
|
||||
void setupBackgroundSync(String password, Wallet2Wallet wallet) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cw_core/hardware/hardware_wallet_service.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -114,3 +115,9 @@ void _logLedgerCommand(Uint8List command, [bool isResponse = true]) {
|
||||
printV("> ${_ledgerMoneroCommands[command[1]]} ${toHexString(command.sublist(2))}");
|
||||
}
|
||||
}
|
||||
|
||||
class MoneroLedgerService extends HardwareWalletService {
|
||||
final LedgerConnection connection;
|
||||
|
||||
MoneroLedgerService(this.connection);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
||||
class MoneroBalance extends Balance {
|
||||
MoneroBalance({
|
||||
required this.fullBalance,
|
||||
required Money unlockedBalance,
|
||||
Money? frozen,
|
||||
}) : super(
|
||||
unlockedBalance,
|
||||
fullBalance - unlockedBalance,
|
||||
frozen: frozen ?? Money.zero(CryptoCurrency.xmr),
|
||||
);
|
||||
|
||||
final Money fullBalance;
|
||||
}
|
||||
@@ -34,8 +34,9 @@ abstract class MoneroSubaddressListBase with Store {
|
||||
try {
|
||||
_isUpdating = true;
|
||||
refresh(accountIndex: accountIndex);
|
||||
final newAddrs = await getAll();
|
||||
subaddresses.clear();
|
||||
subaddresses.addAll(await getAll());
|
||||
subaddresses.addAll(newAddrs);
|
||||
_isUpdating = false;
|
||||
} catch (e) {
|
||||
_isUpdating = false;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/format_amount.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
|
||||
@@ -15,23 +14,10 @@ class MoneroTransactionInfo extends TransactionInfo {
|
||||
final DateTime date;
|
||||
final int accountIndex;
|
||||
final bool isPending;
|
||||
final int amount;
|
||||
final int fee;
|
||||
final Money amount;
|
||||
final Money fee;
|
||||
final int addressIndex;
|
||||
final int confirmations;
|
||||
String? recipientAddress;
|
||||
String? key;
|
||||
String? _fiatAmount;
|
||||
|
||||
@override
|
||||
String amountFormatted() => '${CryptoCurrency.xmr.formatAmount(BigInt.from(amount))} XMR';
|
||||
|
||||
@override
|
||||
String fiatAmount() => _fiatAmount ?? '';
|
||||
|
||||
@override
|
||||
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
|
||||
|
||||
@override
|
||||
String feeFormatted() => '${CryptoCurrency.xmr.formatAmount(BigInt.from(fee))} XMR';
|
||||
}
|
||||
|
||||
@@ -3,12 +3,11 @@ import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:cw_core/monero_amount_format.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cw_core/account.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/monero_balance.dart';
|
||||
import 'package:cw_core/monero_transaction_priority.dart';
|
||||
import 'package:cw_core/monero_wallet_keys.dart';
|
||||
import 'package:cw_core/monero_wallet_utils.dart';
|
||||
@@ -30,6 +29,7 @@ import 'package:cw_monero/api/wallet.dart' as monero_wallet;
|
||||
import 'package:cw_monero/api/wallet_manager.dart';
|
||||
import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart';
|
||||
import 'package:cw_monero/ledger.dart';
|
||||
import 'package:cw_monero/monero_balance.dart';
|
||||
import 'package:cw_monero/monero_transaction_creation_credentials.dart';
|
||||
import 'package:cw_monero/monero_transaction_history.dart';
|
||||
import 'package:cw_monero/monero_transaction_info.dart';
|
||||
@@ -37,6 +37,7 @@ import 'package:cw_monero/monero_unspent.dart';
|
||||
import 'package:cw_monero/monero_wallet_addresses.dart';
|
||||
import 'package:cw_monero/monero_wallet_service.dart';
|
||||
import 'package:cw_monero/pending_monero_transaction.dart';
|
||||
import 'package:cw_monero/trezor.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
|
||||
@@ -119,6 +120,15 @@ abstract class MoneroWalletBase
|
||||
@override
|
||||
bool get hasRescan => true;
|
||||
|
||||
@override
|
||||
bool get hasCoinControl => true;
|
||||
|
||||
@override
|
||||
bool get hasAccountsSupport => true;
|
||||
|
||||
@override
|
||||
bool get hasNativeAccounts => true;
|
||||
|
||||
@override
|
||||
String get seed => monero_wallet.getSeed();
|
||||
String seedLegacy(String? language) => monero_wallet.getSeedLegacy(language);
|
||||
@@ -369,18 +379,53 @@ abstract class MoneroWalletBase
|
||||
};
|
||||
}
|
||||
|
||||
bool needExportOutputs(int amount) {
|
||||
bool hasUnknownKeyImages() => currentWallet!.hasUnknownKeyImages();
|
||||
|
||||
bool needExportOutputs(Money amount) {
|
||||
if (int.tryParse(currentWallet!.secretSpendKey()) != 0) {
|
||||
return false;
|
||||
}
|
||||
// viewOnlyBalance - balance that we can spend
|
||||
// TODO(mrcyjanek): remove hasUnknownKeyImages when we cleanup coin control
|
||||
return (currentWallet!.viewOnlyBalance(accountIndex: walletAddresses.account!.id) < amount) ||
|
||||
return (currentWallet!.viewOnlyBalance(accountIndex: walletAddresses.account!.id) <
|
||||
amount.amount.toInt()) ||
|
||||
currentWallet!.hasUnknownKeyImages();
|
||||
}
|
||||
|
||||
MoneroTrezorService? trezorService;
|
||||
|
||||
Trezor _getTrezor() => Trezor(trezorService ?? (throw Exception("Trezor not connected")));
|
||||
|
||||
Future<void> syncTrezor() async {
|
||||
if (trezorService == null) throw Exception("Trezor not connected");
|
||||
|
||||
final ptr = Pointer<Void>.fromAddress(currentWallet!.ffiAddress());
|
||||
final tdis = monero.Wallet_exportTrezorTdis(ptr);
|
||||
final response = await _getTrezor().keyImageSync(tdis);
|
||||
final success = monero.Wallet_importTrezorEncryptedKeyImagesJson(ptr, response);
|
||||
|
||||
if (!success) throw Exception(monero.Wallet_errorString(ptr));
|
||||
}
|
||||
|
||||
Future<String> signTrezorTransaction(String json) => _getTrezor().signTransaction(json);
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
if (hardwareWalletType == HardwareWalletType.trezor) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
try {
|
||||
return await _createTransaction(credentials);
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
}
|
||||
await save();
|
||||
await Future.delayed(Duration(seconds: i));
|
||||
}
|
||||
}
|
||||
return await _createTransaction(credentials);
|
||||
}
|
||||
|
||||
Future<PendingTransaction> _createTransaction(Object credentials) async {
|
||||
final _credentials = credentials as MoneroTransactionCreationCredentials;
|
||||
final inputs = <String>[];
|
||||
final outputs = _credentials.outputs;
|
||||
@@ -394,9 +439,7 @@ abstract class MoneroWalletBase
|
||||
throw MoneroTransactionCreationException('The wallet is not synced.');
|
||||
}
|
||||
|
||||
if (unspentCoins.isEmpty) {
|
||||
await updateUnspent();
|
||||
}
|
||||
await updateUnspent();
|
||||
|
||||
for (final utx in unspentCoins) {
|
||||
if (utx.isSending) {
|
||||
@@ -405,14 +448,13 @@ abstract class MoneroWalletBase
|
||||
}
|
||||
|
||||
if (hasMultiDestination) {
|
||||
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
|
||||
if (outputs.any((item) => item.sendAll || item.cryptoAmount.amount <= BigInt.zero)) {
|
||||
throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.');
|
||||
}
|
||||
|
||||
final int totalAmount =
|
||||
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
|
||||
final totalAmount = outputs.fold(0, (acc, value) => acc + value.cryptoAmount.amount.toInt());
|
||||
|
||||
if (unlockedBalance < totalAmount) {
|
||||
if (unlockedBalance < Money.fromInt(totalAmount, CryptoCurrency.xmr)) {
|
||||
throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.');
|
||||
}
|
||||
|
||||
@@ -421,8 +463,7 @@ abstract class MoneroWalletBase
|
||||
final moneroOutputs = outputs.map((output) {
|
||||
final outputAddress = output.isParsedAddress ? output.extractedAddress : output.address;
|
||||
|
||||
return MoneroOutput(
|
||||
address: outputAddress!, amount: output.cryptoAmount!.replaceAll(',', '.'));
|
||||
return MoneroOutput(address: outputAddress!, amount: output.cryptoAmount.toString());
|
||||
}).toList();
|
||||
|
||||
pendingTransactionDescription = await transaction_history.createTransactionMultDest(
|
||||
@@ -434,7 +475,12 @@ abstract class MoneroWalletBase
|
||||
} else {
|
||||
final output = outputs.first;
|
||||
final address = output.isParsedAddress ? output.extractedAddress : output.address;
|
||||
final amount = output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.');
|
||||
|
||||
if (!output.sendAll && output.cryptoAmount.amount <= BigInt.zero) {
|
||||
throw MoneroTransactionCreationException('Amount must be greater than 0.');
|
||||
}
|
||||
|
||||
final amount = output.sendAll ? null : output.cryptoAmount.toString();
|
||||
|
||||
// if ((formattedAmount != null && unlockedBalance < formattedAmount) ||
|
||||
// (formattedAmount == null && unlockedBalance <= 0)) {
|
||||
@@ -454,7 +500,10 @@ abstract class MoneroWalletBase
|
||||
paymentId: '');
|
||||
}
|
||||
|
||||
// final status = monero.PendingTransaction_status(pendingTransactionDescription);
|
||||
if (pendingTransactionDescription.txCount != 1) {
|
||||
throw MoneroTransactionCreationException(
|
||||
"This payment would be split into ${pendingTransactionDescription.txCount} transactions. Send smaller transaction to yourself first.",);
|
||||
}
|
||||
|
||||
return PendingMoneroTransaction(pendingTransactionDescription, this);
|
||||
}
|
||||
@@ -611,11 +660,19 @@ abstract class MoneroWalletBase
|
||||
await walletInfo.save();
|
||||
}
|
||||
|
||||
Future<void> updateUnspent() async {
|
||||
Future<void>? _updateUnspentInFlight;
|
||||
|
||||
/// Concurrent callers share one run: two runs at once work on the same [unspentCoins] list, and
|
||||
/// the clean up of the one then deletes the coin infos of the other.
|
||||
Future<void> updateUnspent() => _updateUnspentInFlight ??=
|
||||
_updateUnspent().whenComplete(() => _updateUnspentInFlight = null);
|
||||
|
||||
Future<void> _updateUnspent() async {
|
||||
try {
|
||||
refreshCoins(walletAddresses.account!.id);
|
||||
|
||||
unspentCoins.clear();
|
||||
// Collected separately, so nobody ever sees a half filled list
|
||||
final unspentCoins = <MoneroUnspent>[];
|
||||
|
||||
final coinCount = await countOfCoins();
|
||||
for (var i = 0; i < coinCount; i++) {
|
||||
@@ -633,12 +690,21 @@ abstract class MoneroWalletBase
|
||||
);
|
||||
// TODO: double-check the logic here
|
||||
if (unspent.hash.isNotEmpty) {
|
||||
final tx = await transaction_history.getTransaction(unspent.hash);
|
||||
unspent.isChange = tx.isSpend == true;
|
||||
try {
|
||||
final tx = await transaction_history.getTransaction(unspent.hash);
|
||||
unspent.isChange = tx.isSpend == true;
|
||||
} catch (e) {
|
||||
// The transaction may not be in the (not yet refreshed) history, that must not
|
||||
// keep the output, and all the ones after it, out of coin control.
|
||||
printV("isChange lookup failed for ${unspent.hash}: $e");
|
||||
}
|
||||
}
|
||||
unspentCoins.add(unspent);
|
||||
}
|
||||
}
|
||||
this.unspentCoins
|
||||
..clear()
|
||||
..addAll(unspentCoins);
|
||||
|
||||
if (unspentCoinsInfo.isEmpty) {
|
||||
unspentCoins.forEach((coin) => _addCoinInfo(coin));
|
||||
@@ -646,22 +712,38 @@ abstract class MoneroWalletBase
|
||||
}
|
||||
|
||||
if (unspentCoins.isNotEmpty) {
|
||||
unspentCoins.forEach((coin) {
|
||||
final coinInfoList = unspentCoinsInfo.values.where((element) =>
|
||||
element.walletId.contains(id) &&
|
||||
element.accountIndex == walletAddresses.account!.id &&
|
||||
element.keyImage!.contains(coin.keyImage!));
|
||||
final usedInfoKeys = <dynamic>{};
|
||||
for (final coin in unspentCoins) {
|
||||
final coinInfoList = unspentCoinsInfo.values
|
||||
.where((element) =>
|
||||
element.walletId.contains(id) &&
|
||||
element.accountIndex == walletAddresses.account!.id &&
|
||||
!usedInfoKeys.contains(element.key) &&
|
||||
_isSameCoin(element, coin))
|
||||
.toList();
|
||||
|
||||
if (coinInfoList.isNotEmpty) {
|
||||
final coinInfo = coinInfoList.first;
|
||||
// The key image only breaks a tie between identical outputs of one transaction
|
||||
final coinInfo = coinInfoList.firstWhere(
|
||||
(element) => element.keyImage == coin.keyImage,
|
||||
orElse: () => coinInfoList.first,
|
||||
);
|
||||
usedInfoKeys.add(coinInfo.key);
|
||||
|
||||
coin.isFrozen = coinInfo.isFrozen;
|
||||
coin.isSending = coinInfo.isSending;
|
||||
coin.note = coinInfo.note;
|
||||
|
||||
// The key image of a hardware wallet's output is only known after syncing with the
|
||||
// device; keep the existing info (frozen state, note) instead of orphaning it.
|
||||
if (coinInfo.keyImage != coin.keyImage) {
|
||||
coinInfo.keyImage = coin.keyImage;
|
||||
await coinInfo.save();
|
||||
}
|
||||
} else {
|
||||
_addCoinInfo(coin);
|
||||
await _addCoinInfo(coin);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await _refreshUnspentCoinsInfo();
|
||||
@@ -676,6 +758,12 @@ abstract class MoneroWalletBase
|
||||
}
|
||||
}
|
||||
|
||||
/// The key image can't identify an output: a hardware wallet doesn't know the key images of its
|
||||
/// outputs until they are synced with the device, until then they all carry the same
|
||||
/// placeholder, which made every output match the info of the first one.
|
||||
bool _isSameCoin(UnspentCoinsInfo info, MoneroUnspent coin) =>
|
||||
info.hash == coin.hash && info.address == coin.address && info.value == coin.value;
|
||||
|
||||
Future<void> _addCoinInfo(MoneroUnspent coin) async {
|
||||
final newInfo = UnspentCoinsInfo(
|
||||
walletId: id,
|
||||
@@ -701,8 +789,7 @@ abstract class MoneroWalletBase
|
||||
|
||||
if (currentWalletUnspentCoins.isNotEmpty) {
|
||||
currentWalletUnspentCoins.forEach((element) {
|
||||
final existUnspentCoins =
|
||||
unspentCoins.where((coin) => element.keyImage!.contains(coin.keyImage!));
|
||||
final existUnspentCoins = unspentCoins.where((coin) => _isSameCoin(element, coin));
|
||||
|
||||
if (existUnspentCoins.isEmpty) {
|
||||
keys.add(element.key);
|
||||
@@ -772,10 +859,10 @@ abstract class MoneroWalletBase
|
||||
row.isSpend ? TransactionDirection.outgoing : TransactionDirection.incoming,
|
||||
row.timeStamp,
|
||||
row.isPending,
|
||||
row.amount,
|
||||
Money.fromInt(row.amount, currency),
|
||||
row.accountIndex,
|
||||
0,
|
||||
row.fee,
|
||||
Money.fromInt(row.fee, currency),
|
||||
row.confirmations,
|
||||
)..additionalInfo = <String, dynamic>{
|
||||
'key': row.key,
|
||||
@@ -854,25 +941,25 @@ abstract class MoneroWalletBase
|
||||
final fullBalance = monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id);
|
||||
final frozenBalance = _getFrozenBalance();
|
||||
if (balance[currency]!.fullBalance != fullBalance ||
|
||||
balance[currency]!.unlockedBalance != unlockedBalance ||
|
||||
balance[currency]!.frozenBalance != frozenBalance) {
|
||||
balance[currency]!.available != unlockedBalance ||
|
||||
balance[currency]!.frozen != frozenBalance) {
|
||||
balance[currency] = MoneroBalance(
|
||||
fullBalance: fullBalance, unlockedBalance: unlockedBalance, frozenBalance: frozenBalance);
|
||||
fullBalance: fullBalance, unlockedBalance: unlockedBalance, frozen: frozenBalance);
|
||||
}
|
||||
}
|
||||
|
||||
int _getUnlockedBalance() =>
|
||||
Money _getUnlockedBalance() =>
|
||||
monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id);
|
||||
|
||||
int _getFrozenBalance() {
|
||||
Money _getFrozenBalance() {
|
||||
var frozenBalance = 0;
|
||||
|
||||
for (var coin in unspentCoinsInfo.values.where((element) =>
|
||||
for (final coin in unspentCoinsInfo.values.where((element) =>
|
||||
element.walletId == id && element.accountIndex == walletAddresses.account!.id)) {
|
||||
if (coin.isFrozen && !coin.isSending) frozenBalance += coin.value;
|
||||
}
|
||||
|
||||
return frozenBalance;
|
||||
return Money.fromInt(frozenBalance, CryptoCurrency.xmr);
|
||||
}
|
||||
|
||||
void _onNewBlock(int height, int blocksLeft, double ptc) async {
|
||||
@@ -946,9 +1033,4 @@ abstract class MoneroWalletBase
|
||||
Future<void> setLedgerConnection(LedgerConnection connection) async {
|
||||
await enableLedgerExchange(connection);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatCryptoAmount(String amount) {
|
||||
return moneroAmountToString(amount: int.parse(amount));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
|
||||
addressesMap.clear();
|
||||
addressInfos.clear();
|
||||
|
||||
accountList.accounts.forEach((account) async {
|
||||
for (final account in accountList.accounts) {
|
||||
await _subaddressList.update(accountIndex: account.id);
|
||||
_subaddressList.subaddresses.forEach((subaddress) {
|
||||
addressesMap[subaddress.address] = subaddress.label;
|
||||
@@ -102,7 +102,7 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
|
||||
address: subaddress.address,
|
||||
label: subaddress.label));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await saveAddressesInBox();
|
||||
} catch (e) {
|
||||
|
||||
@@ -3,8 +3,8 @@ import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cw_core/get_height_by_date.dart';
|
||||
import 'package:cw_core/hardware/hardware_wallet_service.dart';
|
||||
import 'package:cw_core/monero_wallet_utils.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
@@ -20,8 +20,8 @@ import 'package:cw_monero/api/wallet_manager.dart';
|
||||
import 'package:cw_monero/bip39_seed.dart';
|
||||
import 'package:cw_monero/ledger.dart';
|
||||
import 'package:cw_monero/monero_wallet.dart';
|
||||
import 'package:cw_monero/trezor.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
|
||||
import 'package:monero/monero.dart' as monero;
|
||||
import 'package:polyseed/polyseed.dart';
|
||||
|
||||
@@ -44,10 +44,15 @@ class MoneroNewWalletCredentials extends WalletCredentials {
|
||||
}
|
||||
|
||||
class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials {
|
||||
MoneroRestoreWalletFromHardwareCredentials(
|
||||
{required String name, required this.ledgerConnection, int height = 0, String? password})
|
||||
: super(name: name, password: password, height: height);
|
||||
LedgerConnection ledgerConnection;
|
||||
MoneroRestoreWalletFromHardwareCredentials({
|
||||
required String name,
|
||||
required this.hardwareWalletService,
|
||||
int height = 0,
|
||||
String? password,
|
||||
String? passphrase,
|
||||
}) : super(name: name, password: password, height: height, passphrase: passphrase);
|
||||
|
||||
final HardwareWalletService hardwareWalletService;
|
||||
}
|
||||
|
||||
class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials {
|
||||
@@ -187,7 +192,7 @@ class MoneroWalletService extends WalletService<
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
password: password);
|
||||
|
||||
if (wallet.isHardwareWallet) {
|
||||
if (wallet.hardwareWalletType == HardwareWalletType.ledger) {
|
||||
wallet.setLedgerConnection(gLedger!);
|
||||
gLedger = null;
|
||||
}
|
||||
@@ -295,12 +300,35 @@ class MoneroWalletService extends WalletService<
|
||||
try {
|
||||
final path = await pathForWallet(name: credentials.name, type: getType());
|
||||
final password = credentials.password;
|
||||
final height = credentials.height;
|
||||
|
||||
enableLedgerExchange(credentials.ledgerConnection);
|
||||
if (credentials.hardwareWalletService case MoneroLedgerService service) {
|
||||
enableLedgerExchange(service.connection);
|
||||
|
||||
await monero_wallet_manager.restoreWalletFromHardwareWallet(
|
||||
path: path, password: password!, restoreHeight: height!, deviceName: 'Ledger');
|
||||
await monero_wallet_manager.restoreWalletFromHardwareWallet(
|
||||
path: path,
|
||||
password: password!,
|
||||
restoreHeight: credentials.height!,
|
||||
deviceName: 'Ledger',
|
||||
);
|
||||
} else if (credentials.hardwareWalletService case final MoneroTrezorService service) {
|
||||
final trezor = Trezor(service);
|
||||
|
||||
final watchCredentials = await trezor.getWatchCredentials();
|
||||
|
||||
monero_wallet_manager.restoreWalletFromKeys(
|
||||
path: path,
|
||||
password: credentials.password!,
|
||||
language: "English",
|
||||
restoreHeight: credentials.height!,
|
||||
address: watchCredentials.address,
|
||||
viewKey: watchCredentials.watchKey,
|
||||
spendKey: "",
|
||||
);
|
||||
|
||||
if (credentials.passphrase != null) {
|
||||
monero_wallet_manager.storePassphrase(path: path, passphrase: credentials.passphrase!);
|
||||
}
|
||||
}
|
||||
|
||||
final wallet = MoneroWallet(
|
||||
walletInfo: credentials.walletInfo!,
|
||||
@@ -549,7 +577,7 @@ class MoneroWalletService extends WalletService<
|
||||
if (walletInfo == null) {
|
||||
return false;
|
||||
}
|
||||
return walletInfo.isHardwareWallet;
|
||||
return walletInfo.hardwareWalletType == HardwareWalletType.ledger;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_monero/api/account_list.dart';
|
||||
import 'package:cw_monero/api/structs/pending_transaction.dart';
|
||||
import 'package:cw_monero/api/transaction_history.dart' as monero_transaction_history;
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/amount_converter.dart';
|
||||
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_monero/api/wallet.dart';
|
||||
import 'package:cw_monero/monero_wallet.dart';
|
||||
import 'package:monero/monero.dart' as monero;
|
||||
|
||||
class DoubleSpendException implements Exception {
|
||||
DoubleSpendException();
|
||||
@@ -24,6 +27,10 @@ class PendingMoneroTransaction with PendingTransaction {
|
||||
final PendingTransactionDescription pendingTransactionDescription;
|
||||
final MoneroWalletBase wallet;
|
||||
|
||||
Money get amount => Money.fromInt(pendingTransactionDescription.amount, CryptoCurrency.xmr);
|
||||
|
||||
Money get fee => Money.fromInt(pendingTransactionDescription.fee, CryptoCurrency.xmr);
|
||||
|
||||
@override
|
||||
String get id => pendingTransactionDescription.hash;
|
||||
|
||||
@@ -31,32 +38,41 @@ class PendingMoneroTransaction with PendingTransaction {
|
||||
String get hex => pendingTransactionDescription.hex;
|
||||
|
||||
@override
|
||||
String get amountFormatted =>
|
||||
AmountConverter.amountIntToString(CryptoCurrency.xmr, pendingTransactionDescription.amount);
|
||||
String get amountFormatted => amount.toString();
|
||||
|
||||
@override
|
||||
String get feeFormatted => "$feeFormattedValue XMR";
|
||||
|
||||
@override
|
||||
String get feeFormattedValue =>
|
||||
AmountConverter.amountIntToString(CryptoCurrency.xmr, pendingTransactionDescription.fee);
|
||||
|
||||
@override
|
||||
bool shouldCommitUR() => isViewOnly;
|
||||
bool shouldCommitUR() => isViewOnly && wallet.hardwareWalletType != HardwareWalletType.trezor;
|
||||
|
||||
@override
|
||||
Future<void> commit() async {
|
||||
try {
|
||||
await monero_transaction_history.commitTransactionFromPointerAddress(
|
||||
address: pendingTransactionDescription.pointerAddress, useUR: false);
|
||||
} catch (e) {
|
||||
final message = e.toString();
|
||||
if (wallet.hardwareWalletType == HardwareWalletType.trezor) {
|
||||
final ptr = Pointer<Void>.fromAddress(pendingTransactionDescription.pointerAddress);
|
||||
final ret = await monero.PendingTransaction_commitTrezor(ptr, 0);
|
||||
|
||||
if (message.contains('Reason: double spend')) {
|
||||
throw DoubleSpendException();
|
||||
final json = await wallet.signTrezorTransaction(ret);
|
||||
|
||||
final wptr = Pointer<Void>.fromAddress(currentWallet!.ffiAddress());
|
||||
|
||||
final suc = await monero.Wallet_submitTransactionHex(wptr, json);
|
||||
|
||||
if (!suc) {
|
||||
final err = monero.UnsignedTransaction_errorString(ptr);
|
||||
printV(err);
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await monero_transaction_history.commitTransactionFromPointerAddress(
|
||||
address: pendingTransactionDescription.pointerAddress, useUR: false);
|
||||
} catch (e) {
|
||||
final message = e.toString();
|
||||
|
||||
rethrow;
|
||||
if (message.contains('Reason: double spend')) {
|
||||
throw DoubleSpendException();
|
||||
}
|
||||
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
storeSync(force: true);
|
||||
unawaited(() async {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import "dart:convert";
|
||||
|
||||
import "package:cw_core/hardware/hardware_wallet_service.dart";
|
||||
import "package:mutex/mutex.dart";
|
||||
import "package:trezor_flutter/trezor_flutter.dart";
|
||||
|
||||
class MoneroTrezorService extends HardwareWalletService {
|
||||
MoneroTrezorService(this.client);
|
||||
|
||||
final TrezorClient client;
|
||||
|
||||
static final Mutex _mutex = Mutex();
|
||||
|
||||
static Future<T> runBlocking<T>(Future<T> Function() operation) => _mutex.protect(operation);
|
||||
}
|
||||
|
||||
class MoneroTrezorWatchCredentials {
|
||||
const MoneroTrezorWatchCredentials(this.watchKey, this.address);
|
||||
|
||||
final String watchKey;
|
||||
final String address;
|
||||
}
|
||||
|
||||
class Trezor {
|
||||
Trezor(this.service);
|
||||
|
||||
final MoneroTrezorService service;
|
||||
|
||||
Future<MoneroTrezorWatchCredentials> getWatchCredentials() async {
|
||||
final credentials = await MoneroTrezorService.runBlocking(TrezorMonero(service.client).getWatchCredentials);
|
||||
|
||||
return MoneroTrezorWatchCredentials(credentials.$1, credentials.$2);
|
||||
}
|
||||
|
||||
Future<String> keyImageSync(String tdis) async {
|
||||
final tdisMap = jsonDecode(tdis) as Map<String, dynamic>;
|
||||
final tdisList = tdisMap["tdis"] as List<dynamic>;
|
||||
|
||||
final txIds = <MoneroKeyImageTxData>[];
|
||||
for (final tdi in tdisList) {
|
||||
txIds.add(
|
||||
MoneroKeyImageTxData(
|
||||
outKey: tdi["out_key"] as String,
|
||||
txPubKey: tdi["tx_pub_key"] as String,
|
||||
internalOutputIndex: tdi["internal_output_index"] as int,
|
||||
subAddrMajor: tdi["sub_addr_major"] as int,
|
||||
subAddrMinor: tdi["sub_addr_minor"] as int,
|
||||
additionalTxPubKeys:
|
||||
(tdi["additional_tx_pub_keys"] as List?)?.map((e) => e as String).toList() ?? [],
|
||||
),
|
||||
);
|
||||
}
|
||||
final keyImages = await MoneroTrezorService.runBlocking(
|
||||
() => TrezorMonero(service.client).syncKeyImages(txIds),
|
||||
);
|
||||
|
||||
return jsonEncode(keyImages.toMap());
|
||||
}
|
||||
|
||||
Future<String> signTransaction(String json) => MoneroTrezorService.runBlocking(
|
||||
() =>
|
||||
TrezorMonero(service.client).signTransaction(jsonDecode(json) as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+13
-8
@@ -16,8 +16,8 @@ dependencies:
|
||||
ffi: ^2.0.1
|
||||
http: ^1.1.0
|
||||
path_provider: ^2.0.11
|
||||
mobx: ^2.0.7+4
|
||||
flutter_mobx: ^2.0.6+1
|
||||
mobx: ^2.6.0
|
||||
flutter_mobx: ^2.3.0
|
||||
intl: any
|
||||
encrypt: ^5.0.1
|
||||
polyseed: ^0.0.7
|
||||
@@ -25,20 +25,25 @@ dependencies:
|
||||
path: ../cw_core
|
||||
monero:
|
||||
git:
|
||||
url: https://github.com/mrcyjanek/monero_c
|
||||
ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2
|
||||
url: https://github.com/mrcyjanek/monero_c.git
|
||||
ref: 33671871de3d51696b66ffb3bd94f744e26974f6
|
||||
path: impls/monero.dart
|
||||
mutex: ^3.1.0
|
||||
ledger_flutter_plus: ^1.4.1
|
||||
trezor_flutter:
|
||||
git:
|
||||
url: https://github.com/cake-tech/trezor-flutter.git
|
||||
ref: 591d4b8c821b9c10561f848b866324a9a8dcb489
|
||||
path: trezor-flutter
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.4.15
|
||||
build_resolvers: ^2.4.4
|
||||
mobx_codegen: ^2.0.7
|
||||
build_runner: ^2.15.0
|
||||
mobx_codegen: ^2.7.7
|
||||
mockito: ^5.4.5
|
||||
hive_generator: ^2.0.1
|
||||
# hive_generator: ^2.0.1
|
||||
hive: 2.0.1
|
||||
sqflite_common_ffi: ^2.3.4+4
|
||||
|
||||
dependency_overrides:
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../pubspec_overrides.yaml
|
||||
Reference in New Issue
Block a user