Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 140 additions & 12 deletions lib/pages/send_view/send_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ class _SendViewState extends ConsumerState<SendView> {
try {
// auto fill address
_address = paymentData.address.trim();
sendToController.text = _address!;

// autofill notes field
if (paymentData.message != null) {
Expand All @@ -180,7 +179,25 @@ class _SendViewState extends ConsumerState<SendView> {
ref.read(pSendAmount.notifier).state = amount;
}

// Extract OP_RETURN data if present (for Rosen Bridge and other protocols)
// Must be set BEFORE sendToController.text to avoid re-entrant
// onChanged handler reading stale null value.
if (paymentData.additionalParams.containsKey('op_return')) {
final data = paymentData.additionalParams['op_return'];
_setOpReturnData(data);
Logging.instance.i(
"Extracted OP_RETURN data from URI, length: ${data!.length ~/ 2} bytes",
);
} else {
_setOpReturnData(null);
}

_setValidAddressProviders(_address);

// Assign controller.text last — it triggers onChanged which depends
// on pOpReturnData already being set above.
sendToController.text = _address!;

setState(() {
_addressToggleFlag = sendToController.text.isNotEmpty;
});
Expand Down Expand Up @@ -524,11 +541,50 @@ class _SendViewState extends ConsumerState<SendView> {
Map<Amount, String> cachedFiroSparkFees = {};
Map<Amount, String> cachedFiroPublicFees = {};

void _setOpReturnData(String? data) {
if (!mounted) {
return;
}
ref.read(pOpReturnData.notifier).state = data;
}

Amount _addOpReturnFeeIfNeeded({
required Amount fee,
required BigInt feeRate,
required FiroWallet wallet,
}) {
final opReturnData = ref.read(pOpReturnData);
if (opReturnData == null ||
opReturnData.isEmpty ||
ref.read(publicPrivateBalanceStateProvider) != BalanceType.public) {
return fee;
}

final extraOutputVSize = AddressUtils.opReturnOutputVSizeFromHex(
opReturnData,
);
final extraFee = wallet.estimateTxFee(
vSize: extraOutputVSize,
feeRatePerKB: feeRate,
);

return fee +
Amount(
rawValue: BigInt.from(extraFee),
fractionDigits: coin.fractionDigits,
);
}

Future<String> calculateFees(Amount amount) async {
final hasOpReturnData =
isFiro &&
ref.read(publicPrivateBalanceStateProvider) == BalanceType.public &&
(ref.read(pOpReturnData)?.isNotEmpty ?? false);

if (isFiro) {
switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
case BalanceType.public:
if (cachedFiroPublicFees[amount] != null) {
if (!hasOpReturnData && cachedFiroPublicFees[amount] != null) {
return cachedFiroPublicFees[amount]!;
}
break;
Expand Down Expand Up @@ -590,10 +646,18 @@ class _SendViewState extends ConsumerState<SendView> {
switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
case BalanceType.public:
fee = await firoWallet.estimateFeeFor(amount, feeRate);
cachedFiroPublicFees[amount] = ref
fee = _addOpReturnFeeIfNeeded(
fee: fee,
feeRate: feeRate,
wallet: firoWallet,
);
final formatted = ref
.read(pAmountFormatter(coin))
.format(fee, withUnitName: true, indicatePrecisionLoss: false);
return cachedFiroPublicFees[amount]!;
if (!hasOpReturnData) {
cachedFiroPublicFees[amount] = formatted;
}
return formatted;

case BalanceType.private:
fee = await firoWallet.estimateFeeForSpark(amount);
Expand Down Expand Up @@ -923,6 +987,7 @@ class _SendViewState extends ConsumerState<SendView> {
selectedUTXOs.isNotEmpty)
? selectedUTXOs
: null,
opReturnData: ref.read(pOpReturnData),
),
);
} else if (wallet is FiroWallet) {
Expand Down Expand Up @@ -964,6 +1029,7 @@ class _SendViewState extends ConsumerState<SendView> {
utxos: (coinControlEnabled && selectedUTXOs.isNotEmpty)
? selectedUTXOs
: null,
opReturnData: ref.read(pOpReturnData),
),
);
}
Expand Down Expand Up @@ -1127,6 +1193,9 @@ class _SendViewState extends ConsumerState<SendView> {
}

void clearSendForm() {
if (!mounted) {
return;
}
sendToController.text = "";
cryptoAmountController.text = "";
baseAmountController.text = "";
Expand All @@ -1136,9 +1205,8 @@ class _SendViewState extends ConsumerState<SendView> {
memoController.text = "";
_address = "";
_addressToggleFlag = false;
if (mounted) {
setState(() {});
}
_setOpReturnData(null);
setState(() {});
}

String _getSendAllTitle(
Expand Down Expand Up @@ -1726,9 +1794,10 @@ class _SendViewState extends ConsumerState<SendView> {
final trimmed = newValue.trim();

if ((trimmed.length -
(_address?.length ?? 0))
.abs() >
1) {
(_address?.length ?? 0))
.abs() >
1 ||
trimmed.contains(':')) {
final parsed =
AddressUtils.parsePaymentUri(
trimmed,
Expand All @@ -1737,6 +1806,7 @@ class _SendViewState extends ConsumerState<SendView> {
if (parsed != null) {
_applyUri(parsed);
} else {
_setOpReturnData(null);
await _checkSparkNameAndOrSetAddress(
newValue,
);
Expand Down Expand Up @@ -1949,6 +2019,38 @@ class _SendViewState extends ConsumerState<SendView> {
),
),
),
if (ref.watch(pOpReturnData) != null &&
_address != null &&
_address!.isNotEmpty &&
(ref.watch(pValidSendToAddress) ||
ref.watch(pValidSparkSendToAddress)) &&
balType == BalanceType.public)
Align(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.only(
left: 12.0,
top: 4.0,
),
child: Tooltip(
message: AddressUtils.formatOpReturnTooltip(
ref.watch(pOpReturnData)!,
),
child: Text(
"Transaction includes metadata "
"(${ref.watch(pOpReturnData)!.length ~/ 2} bytes) "
"\u2014 tap for details",
textAlign: TextAlign.left,
style: STextStyles.label(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorGreen,
),
),
),
),
),
Builder(
builder: (_) {
final String? error;
Expand Down Expand Up @@ -2666,16 +2768,42 @@ class _SendViewState extends ConsumerState<SendView> {
),
const Spacer(),
const SizedBox(height: 12),
if (ref.watch(pOpReturnData) != null &&
balType == BalanceType.private)
Padding(
padding: const EdgeInsets.only(
left: 12.0,
right: 12.0,
bottom: 12.0,
),
child: Text(
"Bridge data detected but Spark (private) "
"transactions cannot carry OP_RETURN data. "
"Switch to public balance to complete the "
"bridge transaction.",
textAlign: TextAlign.left,
style: STextStyles.label(context).copyWith(
color: Theme.of(
context,
).extension<StackColors>()!.textError,
),
),
),
TextButton(
onPressed:
ref.watch(pPreviewTxButtonEnabled(coin))
ref.watch(pPreviewTxButtonEnabled(coin)) &&
(ref.watch(pOpReturnData) == null ||
balType != BalanceType.private)
? isMwcSlatepack
? _createSlatepack
: isEpicSlatepack
? _createEpicSlatepack
: _previewTransaction
: null,
style: ref.watch(pPreviewTxButtonEnabled(coin))
style:
ref.watch(pPreviewTxButtonEnabled(coin)) &&
(ref.watch(pOpReturnData) == null ||
balType != BalanceType.private)
? Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../models/paymint/fee_object_model.dart';
import '../../../providers/providers.dart';
import '../../../providers/ui/fee_rate_type_state_provider.dart';
import '../../../providers/ui/preview_tx_button_state_provider.dart';
import '../../../providers/wallet/public_private_balance_state_provider.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/address_utils.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/amount/amount_formatter.dart';
import '../../../utilities/constants.dart';
Expand Down Expand Up @@ -78,12 +80,54 @@ class _TransactionFeeSelectionSheetState
"Calculating...",
];

Amount _addFiroOpReturnFee({
required Amount fee,
required BigInt feeRate,
required FiroWallet wallet,
required CryptoCurrency coin,
}) {
final opReturnData = ref.read(pOpReturnData);
if (opReturnData == null ||
opReturnData.isEmpty ||
ref.read(publicPrivateBalanceStateProvider) != BalanceType.public) {
return fee;
}

final extraOutputVSize = AddressUtils.opReturnOutputVSizeFromHex(
opReturnData,
);
final extraFee = wallet.estimateTxFee(
vSize: extraOutputVSize,
feeRatePerKB: feeRate,
);

return fee +
Amount(
rawValue: BigInt.from(extraFee),
fractionDigits: coin.fractionDigits,
);
}

Future<Amount> feeFor({
required Amount amount,
required FeeRateType feeRateType,
required BigInt feeRate,
required CryptoCurrency coin,
}) async {
if (!widget.isToken &&
coin is Firo &&
ref.read(publicPrivateBalanceStateProvider) == BalanceType.public &&
(ref.read(pOpReturnData)?.isNotEmpty ?? false)) {
final wallet = ref.read(pWallets).getWallet(walletId) as FiroWallet;
final fee = await wallet.estimateFeeFor(amount, feeRate);
return _addFiroOpReturnFee(
fee: fee,
feeRate: feeRate,
wallet: wallet,
coin: coin,
);
}

switch (feeRateType) {
case FeeRateType.fast:
if (ref.read(feeSheetSessionCacheProvider).fast[amount] == null) {
Expand Down
Loading
Loading