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
93 changes: 87 additions & 6 deletions lib/pages/send_view/send_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,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 @@ -179,7 +178,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'];
ref.read(pOpReturnData.notifier).state = data;
Logging.instance.i(
"Extracted OP_RETURN data from URI, length: ${data!.length ~/ 2} bytes",
);
} else {
ref.read(pOpReturnData.notifier).state = 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 @@ -919,6 +936,7 @@ class _SendViewState extends ConsumerState<SendView> {
selectedUTXOs.isNotEmpty)
? selectedUTXOs
: null,
opReturnData: ref.read(pOpReturnData),
),
);
} else if (wallet is FiroWallet) {
Expand Down Expand Up @@ -960,6 +978,7 @@ class _SendViewState extends ConsumerState<SendView> {
utxos: (coinControlEnabled && selectedUTXOs.isNotEmpty)
? selectedUTXOs
: null,
opReturnData: ref.read(pOpReturnData),
),
);
}
Expand Down Expand Up @@ -1131,6 +1150,7 @@ class _SendViewState extends ConsumerState<SendView> {
memoController.text = "";
_address = "";
_addressToggleFlag = false;
ref.read(pOpReturnData.notifier).state = null;
if (mounted) {
setState(() {});
}
Expand Down Expand Up @@ -1720,9 +1740,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 @@ -1731,6 +1752,8 @@ class _SendViewState extends ConsumerState<SendView> {
if (parsed != null) {
_applyUri(parsed);
} else {
ref.read(pOpReturnData.notifier).state =
null;
await _checkSparkNameAndOrSetAddress(
newValue,
);
Expand Down Expand Up @@ -1943,6 +1966,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 @@ -2660,16 +2715,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 @@ -646,6 +646,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
ref.read(pDesktopUseUTXOs).isNotEmpty)
? ref.read(pDesktopUseUTXOs)
: null,
opReturnData: ref.read(pOpReturnData),
),
);
}
Expand Down Expand Up @@ -915,8 +916,11 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {

if (paymentData != null &&
paymentData.coin?.uriScheme == coin.uriScheme) {
ref.read(pOpReturnData.notifier).state =
paymentData.additionalParams['op_return'];
_applyUri(paymentData);
} else {
ref.read(pOpReturnData.notifier).state = null;
_address = qrCodeData.split("\n").first.trim();
sendToController.text = _address ?? "";

Expand Down Expand Up @@ -1045,8 +1049,11 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
);
if (paymentData != null &&
paymentData.coin?.uriScheme == coin.uriScheme) {
ref.read(pOpReturnData.notifier).state =
paymentData.additionalParams['op_return'];
_applyUri(paymentData);
} else {
ref.read(pOpReturnData.notifier).state = null;
if (coin is Epiccash) {
content = AddressUtils().formatEpicCashAddress(content);
}
Expand All @@ -1063,6 +1070,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
});
}
} catch (e) {
ref.read(pOpReturnData.notifier).state = null;
// If parsing fails, treat it as a plain address.
if (coin is Epiccash) {
// strip http:// and https:// if content contains @
Expand Down Expand Up @@ -1748,14 +1756,18 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
onChanged: (newValue) async {
final trimmed = newValue;

if ((trimmed.length - (_address?.length ?? 0)).abs() > 1) {
if ((trimmed.length - (_address?.length ?? 0)).abs() > 1 ||
trimmed.contains(':')) {
final parsed = AddressUtils.parsePaymentUri(
trimmed,
logging: Logging.instance,
);
if (parsed != null) {
ref.read(pOpReturnData.notifier).state =
parsed.additionalParams['op_return'];
_applyUri(parsed);
} else {
ref.read(pOpReturnData.notifier).state = null;
await _checkSparkNameAndOrSetAddress(newValue);
}
} else {
Expand Down Expand Up @@ -1809,6 +1821,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
onTap: () {
sendToController.text = "";
_address = "";
ref.read(pOpReturnData.notifier).state =
null;
_setValidAddressProviders(_address);
setState(() {
_addressToggleFlag = false;
Expand Down Expand Up @@ -1954,6 +1968,66 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
}
},
),
// OP_RETURN metadata info (green, public mode only, with tooltip)
Builder(
builder: (context) {
final opData = ref.watch(pOpReturnData);
final balType = ref.watch(publicPrivateBalanceStateProvider);
if (opData == null ||
opData.isEmpty ||
balType != BalanceType.public) {
return Container();
}
return Align(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.only(left: 12.0, top: 4.0),
child: Tooltip(
message: AddressUtils.formatOpReturnTooltip(opData),
child: Text(
"Transaction includes metadata "
"(${opData.length ~/ 2} bytes)",
textAlign: TextAlign.left,
style: STextStyles.label(context).copyWith(
color: Theme.of(
context,
).extension<StackColors>()!.accentColorGreen,
),
),
),
),
);
},
),
// OP_RETURN bridge warning (red, private mode only)
Builder(
builder: (context) {
final opData = ref.watch(pOpReturnData);
final balType = ref.watch(publicPrivateBalanceStateProvider);
if (opData == null ||
opData.isEmpty ||
balType != BalanceType.private) {
return Container();
}
return Align(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.only(left: 12.0, top: 4.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,
),
),
),
);
},
),
if (hasOptionalMemo || ref.watch(pValidSparkSendToAddress))
const SizedBox(height: 10),
if (hasOptionalMemo || ref.watch(pValidSparkSendToAddress))
Expand Down
62 changes: 33 additions & 29 deletions lib/providers/ui/preview_tx_button_state_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ final pValidSparkSendToAddress = StateProvider.autoDispose<bool>((_) => false);

final pIsExchangeAddress = StateProvider<bool>((_) => false);

final pOpReturnData = StateProvider<String?>((_) => null);

// MWC Transaction Method Provider.
final pSelectedMwcTransactionMethod = StateProvider<MwcTransactionMethod>(
(_) => MwcTransactionMethod.slatepack,
Expand All @@ -47,42 +49,44 @@ final pIsSlatepack = Provider.family<bool, String>((ref, walletId) {
return false;
});

final pPreviewTxButtonEnabled = Provider.autoDispose
.family<bool, CryptoCurrency>((ref, coin) {
final amount = ref.watch(pSendAmount) ?? Amount.zero;
final pPreviewTxButtonEnabled = Provider.autoDispose.family<bool, CryptoCurrency>(
(ref, coin) {
final amount = ref.watch(pSendAmount) ?? Amount.zero;

// For MWC slatepack transactions, address validation is not required.
if (coin is Mimblewimblecoin) {
final selectedMethod = ref.watch(pSelectedMwcTransactionMethod);
if (selectedMethod == MwcTransactionMethod.slatepack) {
return amount > Amount.zero;
}
// For MWC slatepack transactions, address validation is not required.
if (coin is Mimblewimblecoin) {
final selectedMethod = ref.watch(pSelectedMwcTransactionMethod);
if (selectedMethod == MwcTransactionMethod.slatepack) {
return amount > Amount.zero;
}
}

// For Epic Cash slatepack transactions, address validation is not required.
if (coin is Epiccash) {
final selectedMethod = ref.watch(pSelectedEpicTransactionMethod);
if (selectedMethod == EpicTransactionMethod.slatepack) {
return amount > Amount.zero;
}
// For Epic Cash slatepack transactions, address validation is not required.
if (coin is Epiccash) {
final selectedMethod = ref.watch(pSelectedEpicTransactionMethod);
if (selectedMethod == EpicTransactionMethod.slatepack) {
return amount > Amount.zero;
}
}

if (coin is Firo) {
final firoType = ref.watch(publicPrivateBalanceStateProvider);
switch (firoType) {
case BalanceType.private:
return (ref.watch(pValidSendToAddress) ||
ref.watch(pValidSparkSendToAddress)) &&
!ref.watch(pIsExchangeAddress) &&
amount > Amount.zero;
if (coin is Firo) {
final firoType = ref.watch(publicPrivateBalanceStateProvider);
switch (firoType) {
case BalanceType.private:
return (ref.watch(pValidSendToAddress) ||
ref.watch(pValidSparkSendToAddress)) &&
!ref.watch(pIsExchangeAddress) &&
ref.watch(pOpReturnData) == null &&
amount > Amount.zero;

case BalanceType.public:
return ref.watch(pValidSendToAddress) && amount > Amount.zero;
}
} else {
return ref.watch(pValidSendToAddress) && amount > Amount.zero;
case BalanceType.public:
return ref.watch(pValidSendToAddress) && amount > Amount.zero;
}
});
} else {
return ref.watch(pValidSendToAddress) && amount > Amount.zero;
}
},
);

final previewTokenTxButtonStateProvider = StateProvider.autoDispose<bool>((_) {
return false;
Expand Down
Loading
Loading