From 5a8d3b83f83f90593a61a3a66da780612cf09afa Mon Sep 17 00:00:00 2001 From: NyanCatTW1 <17372086+NyanCatTW1@users.noreply.github.com> Date: Sat, 19 Apr 2025 17:58:50 +0800 Subject: [PATCH] Feat: Add FACT0RN --- .../isar/models/blockchain_data/address.dart | 13 +- lib/services/price.dart | 67 ++-- .../crypto_currency/coins/fact0rn.dart | 241 +++++++++++++ .../crypto_currency/crypto_currency.dart | 1 + lib/wallets/wallet/impl/fact0rn_wallet.dart | 322 ++++++++++++++++++ lib/wallets/wallet/wallet.dart | 115 +++---- scripts/app_config/configure_stack_wallet.sh | 1 + 7 files changed, 658 insertions(+), 102 deletions(-) create mode 100644 lib/wallets/crypto_currency/coins/fact0rn.dart create mode 100644 lib/wallets/wallet/impl/fact0rn_wallet.dart diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index 8947d6c46..cb239bc5e 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -101,7 +101,8 @@ class Address extends CryptoCurrencyAddress { } @override - String toString() => "{ " + String toString() => + "{ " "id: $id, " "walletId: $walletId, " "value: $value, " @@ -130,10 +131,7 @@ class Address extends CryptoCurrencyAddress { return jsonEncode(result); } - static Address fromJsonString( - String jsonString, { - String? overrideWalletId, - }) { + static Address fromJsonString(String jsonString, {String? overrideWalletId}) { final json = jsonDecode(jsonString); final derivationPathString = json["derivationPath"] as String?; @@ -176,7 +174,8 @@ enum AddressType { p2tr, solana, cardanoShelley, - xelis; + xelis, + fact0rn; String get readableName { switch (this) { @@ -216,6 +215,8 @@ enum AddressType { return "Cardano Shelley"; case AddressType.xelis: return "Xelis"; + case AddressType.fact0rn: + return "FACT0RN"; } } } diff --git a/lib/services/price.dart b/lib/services/price.dart index 801e720e2..66a6f23bf 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -37,6 +37,7 @@ class PriceAPI { Epiccash: "epic-cash", Ecash: "ecash", Ethereum: "ethereum", + Fact0rn: "fact0rn", Firo: "zcoin", Monero: "monero", Particl: "particl", @@ -54,13 +55,15 @@ class PriceAPI { static const refreshInterval = 60; // initialize to older than current time minus at least refreshInterval - static DateTime _lastCalled = - DateTime.now().subtract(const Duration(seconds: refreshInterval + 10)); + static DateTime _lastCalled = DateTime.now().subtract( + const Duration(seconds: refreshInterval + 10), + ); static String _lastUsedBaseCurrency = ""; - static const Duration refreshIntervalDuration = - Duration(seconds: refreshInterval); + static const Duration refreshIntervalDuration = Duration( + seconds: refreshInterval, + ); final HTTP client; @@ -85,15 +88,18 @@ class PriceAPI { } } - await DB.instance - .put(boxName: DB.boxNamePriceCache, key: 'cache', value: map); + await DB.instance.put( + boxName: DB.boxNamePriceCache, + key: 'cache', + value: map, + ); } Map> get _cachedPrices { final map = DB.instance.get(boxName: DB.boxNamePriceCache, key: 'cache') - as Map? ?? - {}; + as Map? ?? + {}; // init with 0 final result = { for (final coin in AppConfig.coins) coin: Tuple2(Decimal.zero, 0.0), @@ -132,9 +138,7 @@ class PriceAPI { final externalCalls = Prefs.instance.externalCalls; if ((!Util.isTestEnv && !externalCalls) || !(await Prefs.instance.isExternalCallsSet())) { - Logging.instance.i( - "User does not want to use external calls", - ); + Logging.instance.i("User does not want to use external calls"); return _cachedPrices; } final Map> result = {}; @@ -148,9 +152,10 @@ class PriceAPI { final coinGeckoResponse = await client.get( url: uri, headers: {'Content-Type': 'application/json'}, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); final coinGeckoData = jsonDecode(coinGeckoResponse.body) as List; @@ -160,9 +165,10 @@ class PriceAPI { final coin = AppConfig.getCryptoCurrencyByPrettyName(coinName); final price = Decimal.parse(map["current_price"].toString()); - final change24h = map["price_change_percentage_24h"] != null - ? double.parse(map["price_change_percentage_24h"].toString()) - : 0.0; + final change24h = + map["price_change_percentage_24h"] != null + ? double.parse(map["price_change_percentage_24h"].toString()) + : 0.0; result[coin] = Tuple2(price, change24h); } @@ -172,8 +178,11 @@ class PriceAPI { return _cachedPrices; } catch (e, s) { - Logging.instance - .e("getPricesAnd24hChange($baseCurrency): ", error: e, stackTrace: s); + Logging.instance.e( + "getPricesAnd24hChange($baseCurrency): ", + error: e, + stackTrace: s, + ); // return previous cached values return _cachedPrices; } @@ -185,9 +194,7 @@ class PriceAPI { if ((!Util.isTestEnv && !externalCalls) || !(await Prefs.instance.isExternalCallsSet())) { - Logging.instance.i( - "User does not want to use external calls", - ); + Logging.instance.i("User does not want to use external calls"); return null; } const uriString = @@ -197,9 +204,10 @@ class PriceAPI { final response = await client.get( url: uri, headers: {'Content-Type': 'application/json'}, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); final json = jsonDecode(response.body) as List; @@ -215,21 +223,20 @@ class PriceAPI { } Future>> - getPricesAnd24hChangeForEthTokens({ + getPricesAnd24hChangeForEthTokens({ required Set contractAddresses, required String baseCurrency, }) async { final Map> tokenPrices = {}; if (AppConfig.coins.whereType().isEmpty || - contractAddresses.isEmpty) return tokenPrices; + contractAddresses.isEmpty) + return tokenPrices; final externalCalls = Prefs.instance.externalCalls; if ((!Util.isTestEnv && !externalCalls) || !(await Prefs.instance.isExternalCallsSet())) { - Logging.instance.i( - "User does not want to use external calls", - ); + Logging.instance.i("User does not want to use external calls"); return tokenPrices; } diff --git a/lib/wallets/crypto_currency/coins/fact0rn.dart b/lib/wallets/crypto_currency/coins/fact0rn.dart new file mode 100644 index 000000000..84fc902b5 --- /dev/null +++ b/lib/wallets/crypto_currency/coins/fact0rn.dart @@ -0,0 +1,241 @@ +import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; + +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/node_model.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/default_nodes.dart'; +import '../../../utilities/enums/derive_path_type_enum.dart'; +import '../crypto_currency.dart'; +import '../interfaces/electrumx_currency_interface.dart'; +import '../intermediate/bip39_hd_currency.dart'; + +class Fact0rn extends Bip39HDCurrency with ElectrumXCurrencyInterface { + Fact0rn(super.network) { + _idMain = "fact0rn"; + _uriScheme = "fact0rn"; + switch (network) { + case CryptoCurrencyNetwork.main: + _id = _idMain; + _name = "FACT0RN"; + _ticker = "FACT"; + case CryptoCurrencyNetwork.test: + _id = "fact0rnTestNet"; + _name = "tFACT0RN"; + _ticker = "tFACT"; + default: + throw Exception("Unsupported network: $network"); + } + } + + late final String _id; + @override + String get identifier => _id; + + late final String _idMain; + @override + String get mainNetId => _idMain; + + late final String _name; + @override + String get prettyName => _name; + + late final String _uriScheme; + @override + String get uriScheme => _uriScheme; + + late final String _ticker; + @override + String get ticker => _ticker; + + @override + bool get torSupport => false; + + @override + List get supportedDerivationPathTypes => [ + DerivePathType.bip84, + ]; + + @override + String constructDerivePath({ + required DerivePathType derivePathType, + int account = 0, + required int chain, + required int index, + }) { + String coinType; + + switch (networkParams.wifPrefix) { + case 0x80: + coinType = "42069"; // fact0rn mainnet + break; + case 0xef: + coinType = "1"; // fact0rn testnet + break; + default: + throw Exception("Invalid Fact0rn network wif used!"); + } + + int purpose; + switch (derivePathType) { + case DerivePathType.bip84: + purpose = 84; + break; + + default: + throw Exception("DerivePathType $derivePathType not supported"); + } + + return "m/$purpose'/$coinType'/$account'/$chain/$index"; + } + + @override + Amount get dustLimit => + Amount(rawValue: BigInt.from(1000), fractionDigits: fractionDigits); + + @override + String get genesisHash { + switch (network) { + case CryptoCurrencyNetwork.main: + return "79cb40f8075b0e3dc2bc468c5ce2a7acbe0afd36c6c3d3a134ea692edac7de49"; + case CryptoCurrencyNetwork.test: + return "550bbf0a444d9f92189f067dd225f5b8a5d92587ebc2e8398d143236072580af"; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + ({coinlib.Address address, AddressType addressType}) getAddressForPublicKey({ + required coinlib.ECPublicKey publicKey, + required DerivePathType derivePathType, + }) { + switch (derivePathType) { + case DerivePathType.bip84: + final addr = coinlib.P2WPKHAddress.fromPublicKey( + publicKey, + hrp: networkParams.bech32Hrp, + ); + + return (address: addr, addressType: AddressType.p2wpkh); + + default: + throw Exception("DerivePathType $derivePathType not supported"); + } + } + + @override + int get minConfirms => 1; + + @override + coinlib.Network get networkParams { + switch (network) { + case CryptoCurrencyNetwork.main: + return coinlib.Network( + wifPrefix: 0x80, + p2pkhPrefix: 0x00, + p2shPrefix: 0x05, + privHDPrefix: 0x0488ade4, + pubHDPrefix: 0x0488b21e, + bech32Hrp: "fact", + messagePrefix: '\x18Bitcoin Signed Message:\n', + minFee: BigInt.from(1), // Not used in stack wallet currently + minOutput: dustLimit.raw, // Not used in stack wallet currently + feePerKb: BigInt.from(1), // Not used in stack wallet currently + ); + case CryptoCurrencyNetwork.test: + return coinlib.Network( + wifPrefix: 0xef, + p2pkhPrefix: 0x6f, + p2shPrefix: 0xc4, + privHDPrefix: 0x04358394, + pubHDPrefix: 0x043587cf, + bech32Hrp: "tfact", + messagePrefix: "\x18Bitcoin Signed Message:\n", + minFee: BigInt.from(1), // Not used in stack wallet currently + minOutput: dustLimit.raw, // Not used in stack wallet currently + feePerKb: BigInt.from(1), // Not used in stack wallet currently + ); + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + bool validateAddress(String address) { + try { + coinlib.Address.fromString(address, networkParams); + return true; + } catch (_) { + return false; + } + } + + @override + NodeModel get defaultNode { + switch (network) { + case CryptoCurrencyNetwork.main: + return NodeModel( + host: "electrumx.fact0rn.io", + port: 50002, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(this), + useSSL: true, + enabled: true, + coinName: identifier, + isFailover: true, + isDown: false, + torEnabled: false, + clearnetEnabled: true, + ); + + default: + throw UnimplementedError(); + } + } + + @override + int get defaultSeedPhraseLength => 12; + + @override + int get fractionDigits => 8; + + @override + bool get hasBuySupport => false; + + @override + bool get hasMnemonicPassphraseSupport => true; + + @override + List get possibleMnemonicLengths => [defaultSeedPhraseLength, 24]; + + @override + AddressType get defaultAddressType => defaultDerivePathType.getAddressType(); + + @override + BigInt get satsPerCoin => BigInt.from(100000000); + + @override + int get targetBlockTimeSeconds => 1800; + + @override + DerivePathType get defaultDerivePathType => DerivePathType.bip84; + + @override + Uri defaultBlockExplorer(String txid) { + switch (network) { + case CryptoCurrencyNetwork.main: + // "https://explorer.fact0rn.io/tx/$txid" doesn't show mempool transactions + return Uri.parse("https://factexplorer.io/tx/$txid"); + default: + throw Exception( + "Unsupported network for defaultBlockExplorer(): $network", + ); + } + } + + @override + int get transactionVersion => 2; + + @override + BigInt get defaultFeeRate => BigInt.from(1000); +} diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index d5553ceca..f262490ff 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -12,6 +12,7 @@ export 'coins/dogecoin.dart'; export 'coins/ecash.dart'; export 'coins/epiccash.dart'; export 'coins/ethereum.dart'; +export 'coins/fact0rn.dart'; export 'coins/firo.dart'; export 'coins/litecoin.dart'; export 'coins/monero.dart'; diff --git a/lib/wallets/wallet/impl/fact0rn_wallet.dart b/lib/wallets/wallet/impl/fact0rn_wallet.dart new file mode 100644 index 000000000..14c8106a9 --- /dev/null +++ b/lib/wallets/wallet/impl/fact0rn_wallet.dart @@ -0,0 +1,322 @@ +import 'package:isar/isar.dart'; + +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/isar/models/blockchain_data/transaction.dart'; +import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; +import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; +import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/extensions/extensions.dart'; +import '../../../utilities/logger.dart'; +import '../../crypto_currency/crypto_currency.dart'; +import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; +import '../intermediate/bip39_hd_wallet.dart'; +import '../wallet_mixin_interfaces/coin_control_interface.dart'; +import '../wallet_mixin_interfaces/electrumx_interface.dart'; +import '../wallet_mixin_interfaces/extended_keys_interface.dart'; + +class Fact0rnWallet + extends Bip39HDWallet + with ElectrumXInterface, ExtendedKeysInterface, CoinControlInterface { + Fact0rnWallet(CryptoCurrencyNetwork network) : super(Fact0rn(network) as T); + + @override + int get isarTransactionVersion => 2; + + @override + FilterOperation? get changeAddressFilterOperation => + FilterGroup.and(standardChangeAddressFilters); + + @override + FilterOperation? get receivingAddressFilterOperation => + FilterGroup.and(standardReceivingAddressFilters); + + // =========================================================================== + + @override + Future> fetchAddressesForElectrumXScan() async { + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); + return allAddresses; + } + + // =========================================================================== + + @override + Future updateTransactions() async { + // Get all addresses. + final List
allAddressesOld = + await fetchAddressesForElectrumXScan(); + + // Separate receiving and change addresses. + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); + + // Remove duplicates. + final allAddressesSet = {...receivingAddresses, ...changeAddresses}; + + // Fetch history from ElectrumX. + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); + + // Only parse new txs (not in db yet). + final List> allTransactions = []; + for (final txHash in allTxHashes) { + // Check for duplicates by searching for tx by tx_hash in db. + final storedTx = + await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); + + if (storedTx == null || + storedTx.height == null || + (storedTx.height != null && storedTx.height! <= 0)) { + // Tx not in db yet. + final tx = await electrumXCachedClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + cryptoCurrency: cryptoCurrency, + ); + + // Only tx to list once. + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == + -1) { + tx["height"] = txHash["height"]; + allTransactions.add(tx); + } + } + } + + // Parse all new txs. + final List txns = []; + for (final txData in allTransactions) { + bool wasSentFromThisWallet = false; + // Set to true if any inputs were detected as owned by this wallet. + + bool wasReceivedInThisWallet = false; + // Set to true if any outputs were detected as owned by this wallet. + + // Parse inputs. + BigInt amountReceivedInThisWallet = BigInt.zero; + BigInt changeAmountReceivedInThisWallet = BigInt.zero; + final List inputs = []; + for (final jsonInput in txData["vin"] as List) { + final map = Map.from(jsonInput as Map); + + final List addresses = []; + String valueStringSats = "0"; + OutpointV2? outpoint; + + final coinbase = map["coinbase"] as String?; + + if (coinbase == null) { + // Not a coinbase (ie a typical input). + final txid = map["txid"] as String; + final vout = map["vout"] as int; + + final inputTx = await electrumXCachedClient.getTransaction( + txHash: txid, + cryptoCurrency: cryptoCurrency, + ); + + final prevOutJson = Map.from( + (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) as Map, + ); + + final prevOut = OutputV2.fromElectrumXJson( + prevOutJson, + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + walletOwns: false, // Doesn't matter here as this is not saved. + ); + + outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: txid, + vout: vout, + ); + valueStringSats = prevOut.valueStringSats; + addresses.addAll(prevOut.addresses); + } + + InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: map["scriptSig"]?["hex"] as String?, + scriptSigAsm: map["scriptSig"]?["asm"] as String?, + sequence: map["sequence"] as int?, + outpoint: outpoint, + valueStringSats: valueStringSats, + addresses: addresses, + witness: map["witness"] as String?, + coinbase: coinbase, + innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // Check if input was from this wallet. + if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) { + wasSentFromThisWallet = true; + input = input.copyWith(walletOwns: true); + } + + inputs.add(input); + } + + // Parse outputs. + final List outputs = []; + for (final outputJson in txData["vout"] as List) { + OutputV2 output = OutputV2.fromElectrumXJson( + Map.from(outputJson as Map), + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // If output was to my wallet, add value to amount received. + if (receivingAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + amountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } else if (changeAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + changeAmountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } + + outputs.add(output); + } + + final totalOut = outputs + .map((e) => e.value) + .fold(BigInt.zero, (value, element) => value + element); + + TransactionType type; + final TransactionSubType subType = TransactionSubType.none; + + // At least one input was owned by this wallet. + if (wasSentFromThisWallet) { + type = TransactionType.outgoing; + + if (wasReceivedInThisWallet) { + if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet == + totalOut) { + // Definitely sent all to self. + type = TransactionType.sentToSelf; + } else if (amountReceivedInThisWallet == BigInt.zero) { + // Most likely just a typical send, do nothing here yet. + } + + // Fact0rn has special outputs like deadpool bounties + announcements, but they're unsupported. + // This is where we would check for them. + // TODO: [prio=none] Check for special Fact0rn outputs. + } + } else if (wasReceivedInThisWallet) { + // Only found outputs owned by this wallet. + type = TransactionType.incoming; + } else { + Logging.instance.e("Unexpected tx found (ignoring it)"); + Logging.instance.d("Unexpected tx found (ignoring it): $txData"); + continue; + } + + final tx = TransactionV2( + walletId: walletId, + blockHash: txData["blockhash"] as String?, + hash: txData["hash"] as String, + txid: txData["txid"] as String, + height: txData["height"] as int?, + version: txData["version"] as int, + timestamp: + txData["blocktime"] as int? ?? + DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + type: type, + subType: subType, + otherData: null, + ); + + txns.add(tx); + } + + await mainDB.updateOrPutTransactionV2s(txns); + } + + @override + Future<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( + Map jsonUTXO, + String? scriptPubKeyHex, + Map jsonTX, + String? utxoOwnerAddress, + ) async { + bool blocked = false; + String? blockedReason; + + // check for bip47 notification + final outputs = jsonTX["vout"] as List; + for (final output in outputs) { + final List? scriptChunks = + (output['scriptPubKey']?['asm'] as String?)?.split(" "); + if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") { + final blindedPaymentCode = scriptChunks![1]; + final bytes = blindedPaymentCode.toUint8ListFromHex; + + // https://en.bitcoin.it/wiki/BIP_0047#Sending + if (bytes.length == 80 && bytes.first == 1) { + blocked = true; + blockedReason = + "Paynym notification output. Incautious " + "handling of outputs from notification transactions " + "may cause unintended loss of privacy."; + break; + } + } + } + + return (blockedReason: blockedReason, blocked: blocked, utxoLabel: null); + } + + // Typical SegWit estimation + @override + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil(), + ), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + @override + int estimateTxFee({required int vSize, required int feeRatePerKB}) { + return vSize * (feeRatePerKB / 1000).ceil(); + } +} diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index be21c5282..c2e542eb7 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -35,6 +35,7 @@ import 'impl/dogecoin_wallet.dart'; import 'impl/ecash_wallet.dart'; import 'impl/epiccash_wallet.dart'; import 'impl/ethereum_wallet.dart'; +import 'impl/fact0rn_wallet.dart'; import 'impl/firo_wallet.dart'; import 'impl/litecoin_wallet.dart'; import 'impl/monero_wallet.dart'; @@ -127,11 +128,7 @@ abstract class Wallet { await updateChainHeight(); } catch (e, s) { // do nothing on failure (besides logging) - Logging.instance.w( - "$e\n$s", - error: e, - stackTrace: s, - ); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); } // return regardless of whether it was updated or not as we want a @@ -173,7 +170,8 @@ abstract class Wallet { value: viewOnlyData!.toJsonEncodedString(), ); } else if (wallet is MnemonicInterface) { - if (wallet is CryptonoteWallet || wallet is XelisWallet) { // + if (wallet is CryptonoteWallet || wallet is XelisWallet) { + // // currently a special case due to the xmr/wow/xelis libraries handling their // own mnemonic generation on new wallet creation // if its a restore we must set them @@ -238,10 +236,11 @@ abstract class Wallet { required NodeService nodeService, required Prefs prefs, }) async { - final walletInfo = await mainDB.isar.walletInfo - .where() - .walletIdEqualTo(walletId) - .findFirst(); + final walletInfo = + await mainDB.isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .findFirst(); Logging.instance.i( "Wallet.load loading" @@ -270,10 +269,7 @@ abstract class Wallet { required EthereumWallet ethWallet, required EthContract contract, }) { - final Wallet wallet = EthTokenWallet( - ethWallet, - contract, - ); + final Wallet wallet = EthTokenWallet(ethWallet, contract); wallet.prefs = ethWallet.prefs; wallet.nodeService = ethWallet.nodeService; @@ -287,27 +283,19 @@ abstract class Wallet { // ========== Static Util ==================================================== // secure storage key - static String mnemonicKey({ - required String walletId, - }) => + static String mnemonicKey({required String walletId}) => "${walletId}_mnemonic"; // secure storage key - static String mnemonicPassphraseKey({ - required String walletId, - }) => + static String mnemonicPassphraseKey({required String walletId}) => "${walletId}_mnemonicPassphrase"; // secure storage key - static String privateKeyKey({ - required String walletId, - }) => + static String privateKeyKey({required String walletId}) => "${walletId}_privateKey"; // secure storage key - static String getViewOnlyWalletDataSecStoreKey({ - required String walletId, - }) => + static String getViewOnlyWalletDataSecStoreKey({required String walletId}) => "${walletId}_viewOnlyWalletData"; //============================================================================ @@ -321,9 +309,7 @@ abstract class Wallet { required NodeService nodeService, required Prefs prefs, }) async { - final Wallet wallet = _loadWallet( - walletInfo: walletInfo, - ); + final Wallet wallet = _loadWallet(walletInfo: walletInfo); wallet.prefs = prefs; wallet.nodeService = nodeService; @@ -339,9 +325,7 @@ abstract class Wallet { .._walletId = walletInfo.walletId; } - static Wallet _loadWallet({ - required WalletInfo walletInfo, - }) { + static Wallet _loadWallet({required WalletInfo walletInfo}) { final net = walletInfo.coin.network; switch (walletInfo.coin.runtimeType) { case const (Banano): @@ -374,6 +358,9 @@ abstract class Wallet { case const (Ethereum): return EthereumWallet(net); + case const (Fact0rn): + return Fact0rnWallet(net); + case const (Firo): return FiroWallet(net); @@ -421,12 +408,11 @@ abstract class Wallet { _periodicPingCheck(); // then periodically check - _networkAliveTimer = Timer.periodic( - Constants.networkAliveTimerDuration, - (_) async { - _periodicPingCheck(); - }, - ); + _networkAliveTimer = Timer.periodic(Constants.networkAliveTimerDuration, ( + _, + ) async { + _periodicPingCheck(); + }); } void _periodicPingCheck() async { @@ -438,15 +424,12 @@ abstract class Wallet { final bool hasNetwork = await pingCheck(); if (_isConnected != hasNetwork) { - final NodeConnectionStatus status = hasNetwork - ? NodeConnectionStatus.connected - : NodeConnectionStatus.disconnected; + final NodeConnectionStatus status = + hasNetwork + ? NodeConnectionStatus.connected + : NodeConnectionStatus.disconnected; GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - status, - walletId, - cryptoCurrency, - ), + NodeConnectionStatusChangedEvent(status, walletId, cryptoCurrency), ); _isConnected = hasNetwork; @@ -518,7 +501,8 @@ abstract class Wallet { } NodeModel getCurrentNode() { - final node = nodeService.getPrimaryNodeFor(currency: cryptoCurrency) ?? + final node = + nodeService.getPrimaryNodeFor(currency: cryptoCurrency) ?? cryptoCurrency.defaultNode; return node; @@ -538,8 +522,9 @@ abstract class Wallet { ); if (shouldAutoSync) { - _periodicRefreshTimer ??= - Timer.periodic(const Duration(seconds: 150), (timer) async { + _periodicRefreshTimer ??= Timer.periodic(const Duration(seconds: 150), ( + timer, + ) async { // chain height check currently broken // if ((await chainHeight) != (await storedChainHeight)) { @@ -596,7 +581,8 @@ abstract class Wallet { } final start = DateTime.now(); - final viewOnly = this is ViewOnlyOptionInterface && + final viewOnly = + this is ViewOnlyOptionInterface && (this as ViewOnlyOptionInterface).isViewOnly; try { @@ -621,8 +607,9 @@ abstract class Wallet { final Set codesToCheck = {}; if (this is PaynymInterface && !viewOnly) { // isSegwit does not matter here at all - final myCode = - await (this as PaynymInterface).getPaymentCode(isSegwit: false); + final myCode = await (this as PaynymInterface).getPaymentCode( + isSegwit: false, + ); final nym = await PaynymIsApi().nym(myCode.toString()); if (nym.value != null) { @@ -685,8 +672,9 @@ abstract class Wallet { // TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided. if (!viewOnly && this is PaynymInterface && codesToCheck.isNotEmpty) { - await (this as PaynymInterface) - .checkForNotificationTransactionsTo(codesToCheck); + await (this as PaynymInterface).checkForNotificationTransactionsTo( + codesToCheck, + ); // check utxos again for notification outputs await updateUTXOs(); } @@ -746,10 +734,11 @@ abstract class Wallet { // Check if there's another wallet of this coin on the sync list. final List walletIds = []; for (final id in prefs.walletIdsSyncOnStartup) { - final wallet = mainDB.isar.walletInfo - .where() - .walletIdEqualTo(id) - .findFirstSync()!; + final wallet = + mainDB.isar.walletInfo + .where() + .walletIdEqualTo(id) + .findFirstSync()!; if (wallet.coin == cryptoCurrency) { walletIds.add(id); @@ -802,17 +791,11 @@ abstract class Wallet { return await mainDB.isar.addresses .buildQuery
( whereClauses: [ - IndexWhereClause.equalTo( - indexName: r"walletId", - value: [walletId], - ), + IndexWhereClause.equalTo(indexName: r"walletId", value: [walletId]), ], filter: filterOperation, sortBy: [ - const SortProperty( - property: r"derivationIndex", - sort: Sort.desc, - ), + const SortProperty(property: r"derivationIndex", sort: Sort.desc), ], ) .findFirst(); diff --git a/scripts/app_config/configure_stack_wallet.sh b/scripts/app_config/configure_stack_wallet.sh index e46420fa0..72e4cc8e9 100755 --- a/scripts/app_config/configure_stack_wallet.sh +++ b/scripts/app_config/configure_stack_wallet.sh @@ -63,6 +63,7 @@ final List _supportedCoins = List.unmodifiable([ Ecash(CryptoCurrencyNetwork.main), Epiccash(CryptoCurrencyNetwork.main), Ethereum(CryptoCurrencyNetwork.main), + Fact0rn(CryptoCurrencyNetwork.main), Firo(CryptoCurrencyNetwork.main), Litecoin(CryptoCurrencyNetwork.main), Nano(CryptoCurrencyNetwork.main),