diff --git a/lib/models/coinlib/exp2pkh_address.dart b/lib/models/coinlib/exp2pkh_address.dart new file mode 100644 index 000000000..839c5f124 --- /dev/null +++ b/lib/models/coinlib/exp2pkh_address.dart @@ -0,0 +1,87 @@ +import 'dart:typed_data'; + +import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; + +const OP_EXCHANGEADDR = 0xe0; + +class EXP2PKHAddress implements coinlib.Address { + /// The 160bit public key or redeemScript hash for the base58 address + final Uint8List _hash; + + /// The network and address type version of the address + final Uint8List version; + + String? _encodedCache; + + EXP2PKHAddress._(Uint8List hash, this.version) : _hash = hash { + if (version.length != 3) { + throw ArgumentError( + "version bytes length must be 3", + ); + } + } + + factory EXP2PKHAddress.fromString(String encoded, Uint8List versionBytes) { + if (versionBytes.length != 3) { + throw ArgumentError( + "version bytes length must be 3", + ); + } + + final data = coinlib.base58Decode(encoded); + if (data.length != 23) throw coinlib.InvalidAddress(); + + final version = data.sublist(0, 3); + + for (int i = 0; i < 3; i++) { + if (version[i] != versionBytes[i]) { + throw Exception("EX address version bytes do not match"); + } + } + + final payload = data.sublist(3); + + final addr = EXP2PKHAddress._(payload, version); + + addr._encodedCache = encoded; + return addr; + } + + @override + String toString() => _encodedCache.toString(); + + @override + coinlib.Program get program => EXP2PKH.fromHash(_hash); +} + +class EXP2PKH implements coinlib.Program { + static const template = + "OP_EXCHANGEADDR OP_DUP OP_HASH160 <20-bytes> OP_EQUALVERIFY OP_CHECKSIG"; + + @override + final coinlib.Script script; + + EXP2PKH.fromScript(this.script); + + factory EXP2PKH.fromHash(Uint8List pkHash) { + final List ops = [ + coinlib.ScriptOpCode(OP_EXCHANGEADDR), + ]; + final parts = template.split(" ").sublist(1); + for (final name in parts) { + if (name.startsWith("OP_")) { + ops.add( + coinlib.ScriptOpCode( + coinlib.scriptOpNameToCode[name.substring(3)]!, + ), + ); + } else if (name == "<20-bytes>") { + ops.add(coinlib.ScriptPushData(pkHash)); + } else { + throw Exception("Something went wrong in this hacked code"); + } + } + + return EXP2PKH.fromScript(coinlib.Script(ops)); + } +} diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 376f66d9d..9d1d4f8fc 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -56,6 +56,7 @@ import '../../widgets/animated_text.dart'; import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/custom_buttons/blue_text_button.dart'; +import '../../widgets/dialogs/firo_exchange_address_dialog.dart'; import '../../widgets/fee_slider.dart'; import '../../widgets/icon_widgets/addressbook_icon.dart'; import '../../widgets/icon_widgets/clipboard_icon.dart'; @@ -394,6 +395,14 @@ class _SendViewState extends ConsumerState { address: address ?? "", isTestNet: wallet.cryptoCurrency.network.isTestNet, ); + + ref.read(pIsExchangeAddress.state).state = + (coin as Firo).isExchangeAddress(_address ?? ""); + + if (ref.read(publicPrivateBalanceStateProvider) == FiroType.spark && + ref.read(pIsExchangeAddress)) { + showFiroExchangeAddressWarning(context); + } } ref.read(pValidSendToAddress.notifier).state = @@ -875,7 +884,10 @@ class _SendViewState extends ConsumerState { @override void initState() { coin = widget.coin; - ref.refresh(feeSheetSessionCacheProvider); + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.refresh(feeSheetSessionCacheProvider); + ref.refresh(pIsExchangeAddress); + }); _currentFee = 0.toAmountAsRaw(fractionDigits: coin.fractionDigits); _calculateFeesFuture = @@ -1003,6 +1015,8 @@ class _SendViewState extends ConsumerState { : true); if (isFiro) { + final isExchangeAddress = ref.watch(pIsExchangeAddress); + ref.listen(publicPrivateBalanceStateProvider, (previous, next) { selectedUTXOs = {}; @@ -1019,6 +1033,12 @@ class _SendViewState extends ConsumerState { ); }); } + + if (previous != next && next == FiroType.spark && isExchangeAddress) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => showFiroExchangeAddressWarning(context), + ); + } }); } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index d54e262f6..57ba79d68 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -60,6 +60,7 @@ import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../../widgets/desktop/desktop_fee_dialog.dart'; import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/secondary_button.dart'; +import '../../../../widgets/dialogs/firo_exchange_address_dialog.dart'; import '../../../../widgets/fee_slider.dart'; import '../../../../widgets/icon_widgets/addressbook_icon.dart'; import '../../../../widgets/icon_widgets/clipboard_icon.dart'; @@ -706,6 +707,9 @@ class _DesktopSendState extends ConsumerState { address: address ?? "", isTestNet: wallet.cryptoCurrency.network.isTestNet, ); + + ref.read(pIsExchangeAddress.state).state = + (coin as Firo).isExchangeAddress(_address ?? ""); } ref.read(pValidSendToAddress.notifier).state = @@ -842,6 +846,7 @@ class _DesktopSendState extends ConsumerState { void initState() { WidgetsBinding.instance.addPostFrameCallback((_) { ref.refresh(feeSheetSessionCacheProvider); + ref.refresh(pIsExchangeAddress); ref.read(pValidSendToAddress.state).state = false; ref.read(pValidSparkSendToAddress.state).state = false; }); @@ -944,15 +949,22 @@ class _DesktopSendState extends ConsumerState { }); } + final firoType = ref.watch(publicPrivateBalanceStateProvider); + if (coin is Firo && firoType == FiroType.spark) { + if (ref.watch(pIsExchangeAddress)) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => showFiroExchangeAddressWarning(context), + ); + } + } + final showCoinControl = ref.watch( prefsChangeNotifierProvider.select( (value) => value.enableCoinControl, ), ) && ref.watch(pWallets).getWallet(walletId) is CoinControlInterface && - (coin is Firo - ? ref.watch(publicPrivateBalanceStateProvider) == FiroType.public - : true); + (coin is Firo ? firoType == FiroType.public : true); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -978,7 +990,7 @@ class _DesktopSendState extends ConsumerState { DropdownButtonHideUnderline( child: DropdownButton2( isExpanded: true, - value: ref.watch(publicPrivateBalanceStateProvider.state).state, + value: firoType, items: [ DropdownMenuItem( value: FiroType.spark, @@ -1464,8 +1476,7 @@ class _DesktopSendState extends ConsumerState { if (_address == null || _address!.isEmpty) { error = null; } else if (coin is Firo) { - if (ref.watch(publicPrivateBalanceStateProvider) == - FiroType.lelantus) { + if (firoType == FiroType.lelantus) { if (_data != null && _data!.contactLabel == _address) { error = SparkInterface.validateSparkAddress( address: _data!.address, @@ -1526,15 +1537,13 @@ class _DesktopSendState extends ConsumerState { ), if (isStellar || (ref.watch(pValidSparkSendToAddress) && - ref.watch(publicPrivateBalanceStateProvider) != - FiroType.lelantus)) + firoType != FiroType.lelantus)) const SizedBox( height: 10, ), if (isStellar || (ref.watch(pValidSparkSendToAddress) && - ref.watch(publicPrivateBalanceStateProvider) != - FiroType.lelantus)) + firoType != FiroType.lelantus)) ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, diff --git a/lib/providers/ui/preview_tx_button_state_provider.dart b/lib/providers/ui/preview_tx_button_state_provider.dart index 89d960743..196fe298d 100644 --- a/lib/providers/ui/preview_tx_button_state_provider.dart +++ b/lib/providers/ui/preview_tx_button_state_provider.dart @@ -9,27 +9,37 @@ */ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../wallet/public_private_balance_state_provider.dart'; + import '../../utilities/amount/amount.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; +import '../wallet/public_private_balance_state_provider.dart'; final pSendAmount = StateProvider.autoDispose((_) => null); final pValidSendToAddress = StateProvider.autoDispose((_) => false); final pValidSparkSendToAddress = StateProvider.autoDispose((_) => false); +final pIsExchangeAddress = StateProvider((_) => false); + final pPreviewTxButtonEnabled = Provider.autoDispose.family((ref, coin) { final amount = ref.watch(pSendAmount) ?? Amount.zero; if (coin is Firo) { - if (ref.watch(publicPrivateBalanceStateProvider) == FiroType.lelantus) { - return ref.watch(pValidSendToAddress) && - !ref.watch(pValidSparkSendToAddress) && - amount > Amount.zero; - } else { - return (ref.watch(pValidSendToAddress) || - ref.watch(pValidSparkSendToAddress)) && - amount > Amount.zero; + final firoType = ref.watch(publicPrivateBalanceStateProvider); + switch (firoType) { + case FiroType.lelantus: + return ref.watch(pValidSendToAddress) && + !ref.watch(pValidSparkSendToAddress) && + amount > Amount.zero; + + case FiroType.spark: + return (ref.watch(pValidSendToAddress) || + ref.watch(pValidSparkSendToAddress)) && + !ref.watch(pIsExchangeAddress) && + amount > Amount.zero; + + case FiroType.public: + return ref.watch(pValidSendToAddress) && amount > Amount.zero; } } else { return ref.watch(pValidSendToAddress) && amount > Amount.zero; diff --git a/lib/wallets/crypto_currency/coins/firo.dart b/lib/wallets/crypto_currency/coins/firo.dart index cf36840c7..9e4a3bddf 100644 --- a/lib/wallets/crypto_currency/coins/firo.dart +++ b/lib/wallets/crypto_currency/coins/firo.dart @@ -1,5 +1,8 @@ +import 'dart:typed_data'; + import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; +import '../../../models/coinlib/exp2pkh_address.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/node_model.dart'; import '../../../utilities/amount/amount.dart'; @@ -77,6 +80,21 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface { fractionDigits: fractionDigits, ); + Uint8List get exAddressVersion { + switch (network) { + case CryptoCurrencyNetwork.main: + // https://github.com/firoorg/firo/blob/master/src/chainparams.cpp#L357 + return Uint8List.fromList([0x01, 0xb9, 0xbb]); + + case CryptoCurrencyNetwork.test: + // https://github.com/firoorg/firo/blob/master/src/chainparams.cpp#L669 + return Uint8List.fromList([0x01, 0xb9, 0xb1]); + + default: + throw Exception("Unsupported network: $network"); + } + } + @override coinlib.Network get networkParams { switch (network) { @@ -169,7 +187,11 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface { coinlib.Address.fromString(address, networkParams); return true; } catch (_) { - return validateSparkAddress(address); + if (validateSparkAddress(address)) { + return true; + } else { + return isExchangeAddress(address); + } } } @@ -180,6 +202,18 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface { ); } + bool isExchangeAddress(String address) { + try { + EXP2PKHAddress.fromString( + address, + exAddressVersion, + ); + return true; + } catch (_) { + return false; + } + } + @override NodeModel get defaultNode { switch (network) { diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 5858517fd..c7b8aa259 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -8,6 +8,7 @@ import 'package:isar/isar.dart'; import '../../../electrumx_rpc/cached_electrumx_client.dart'; import '../../../electrumx_rpc/client_manager.dart'; import '../../../electrumx_rpc/electrumx_client.dart'; +import '../../../models/coinlib/exp2pkh_address.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'; @@ -24,6 +25,7 @@ import '../../crypto_currency/coins/firo.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../../models/tx_data.dart'; import '../impl/bitcoin_wallet.dart'; +import '../impl/firo_wallet.dart'; import '../impl/peercoin_wallet.dart'; import '../intermediate/bip39_hd_wallet.dart'; import 'cpfp_interface.dart'; @@ -725,11 +727,23 @@ mixin ElectrumXInterface // Add transaction output for (var i = 0; i < txData.recipients!.length; i++) { - final address = coinlib.Address.fromString( - normalizeAddress(txData.recipients![i].address), - cryptoCurrency.networkParams, - ); + late final coinlib.Address address; + try { + address = coinlib.Address.fromString( + normalizeAddress(txData.recipients![i].address), + cryptoCurrency.networkParams, + ); + } catch (_) { + if (this is FiroWallet) { + address = EXP2PKHAddress.fromString( + normalizeAddress(txData.recipients![i].address), + (cryptoCurrency as Firo).exAddressVersion, + ); + } else { + rethrow; + } + } final output = coinlib.Output.fromAddress( txData.recipients![i].amount.raw, address, diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 90f7cfe31..8ebebc7ca 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -716,13 +716,13 @@ mixin SparkInterface return result; } catch (e) { Logging.instance.log( - "refreshSparkMempoolData() failed: $e", + "_refreshSparkCoinsMempoolCheck() failed: $e", level: LogLevel.Error, ); return []; } finally { Logging.instance.log( - "$walletId ${info.name} refreshSparkCoinsMempoolCheck() run " + "$walletId ${info.name} _refreshSparkCoinsMempoolCheck() run " "duration: ${DateTime.now().difference(start)}", level: LogLevel.Debug, ); diff --git a/lib/widgets/dialogs/firo_exchange_address_dialog.dart b/lib/widgets/dialogs/firo_exchange_address_dialog.dart new file mode 100644 index 000000000..fc30c43d4 --- /dev/null +++ b/lib/widgets/dialogs/firo_exchange_address_dialog.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import '../../utilities/util.dart'; +import '../stack_dialog.dart'; + +class FiroExchangeAddressDialog extends StatelessWidget { + const FiroExchangeAddressDialog({super.key}); + + @override + Widget build(BuildContext context) { + return StackOkDialog( + title: "Firo exchange address detected", + message: "Sending to an exchange address from a Spark balance is not" + " allowed. Please send from your transparent balance.", + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 500 : null, + ); + } +} + +Future showFiroExchangeAddressWarning(BuildContext context) async { + return await showDialog( + context: context, + builder: (_) => const FiroExchangeAddressDialog(), + ); +} diff --git a/pubspec.lock b/pubspec.lock index 47107034f..7a25ed30b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1807,8 +1807,8 @@ packages: dependency: "direct main" description: path: "." - ref: f1d02f7ad489df3119a540a7f31485db6d837843 - resolved-ref: f1d02f7ad489df3119a540a7f31485db6d837843 + ref: "647cadc3c82c276dc07915b02d24538fd610f220" + resolved-ref: "647cadc3c82c276dc07915b02d24538fd610f220" url: "https://github.com/cypherstack/tor.git" source: git version: "0.0.1"