WIP firo exchange addresses

This commit is contained in:
julian 2024-06-25 14:46:36 -06:00
parent 8639309e70
commit f634ce8701
9 changed files with 229 additions and 29 deletions

View file

@ -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<coinlib.ScriptOp> 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));
}
}

View file

@ -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<SendView> {
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<SendView> {
@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<SendView> {
: true);
if (isFiro) {
final isExchangeAddress = ref.watch(pIsExchangeAddress);
ref.listen(publicPrivateBalanceStateProvider, (previous, next) {
selectedUTXOs = {};
@ -1019,6 +1033,12 @@ class _SendViewState extends ConsumerState<SendView> {
);
});
}
if (previous != next && next == FiroType.spark && isExchangeAddress) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => showFiroExchangeAddressWarning(context),
);
}
});
}

View file

@ -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<DesktopSend> {
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<DesktopSend> {
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<DesktopSend> {
});
}
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<DesktopSend> {
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<DesktopSend> {
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<DesktopSend> {
),
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,

View file

@ -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<Amount?>((_) => null);
final pValidSendToAddress = StateProvider.autoDispose<bool>((_) => false);
final pValidSparkSendToAddress = StateProvider.autoDispose<bool>((_) => false);
final pIsExchangeAddress = StateProvider<bool>((_) => false);
final pPreviewTxButtonEnabled =
Provider.autoDispose.family<bool, CryptoCurrency>((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;

View file

@ -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) {

View file

@ -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<T extends ElectrumXCurrencyInterface>
// 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,

View file

@ -716,13 +716,13 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
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,
);

View file

@ -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<void> showFiroExchangeAddressWarning(BuildContext context) async {
return await showDialog<void>(
context: context,
builder: (_) => const FiroExchangeAddressDialog(),
);
}

View file

@ -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"