@@ -160,7 +160,6 @@ class _SendViewState extends ConsumerState<SendView> {
160160 try {
161161 // auto fill address
162162 _address = paymentData.address.trim ();
163- sendToController.text = _address! ;
164163
165164 // autofill notes field
166165 if (paymentData.message != null ) {
@@ -180,7 +179,25 @@ class _SendViewState extends ConsumerState<SendView> {
180179 ref.read (pSendAmount.notifier).state = amount;
181180 }
182181
182+ // Extract OP_RETURN data if present (for Rosen Bridge and other protocols)
183+ // Must be set BEFORE sendToController.text to avoid re-entrant
184+ // onChanged handler reading stale null value.
185+ if (paymentData.additionalParams.containsKey ('op_return' )) {
186+ final data = paymentData.additionalParams['op_return' ];
187+ _setOpReturnData (data);
188+ Logging .instance.i (
189+ "Extracted OP_RETURN data from URI, length: ${data !.length ~/ 2 } bytes" ,
190+ );
191+ } else {
192+ _setOpReturnData (null );
193+ }
194+
183195 _setValidAddressProviders (_address);
196+
197+ // Assign controller.text last — it triggers onChanged which depends
198+ // on pOpReturnData already being set above.
199+ sendToController.text = _address! ;
200+
184201 setState (() {
185202 _addressToggleFlag = sendToController.text.isNotEmpty;
186203 });
@@ -240,6 +257,7 @@ class _SendViewState extends ConsumerState<SendView> {
240257 paymentData.coin? .uriScheme == coin.uriScheme) {
241258 _applyUri (paymentData);
242259 } else {
260+ _setOpReturnData (null );
243261 if (coin is Epiccash ) {
244262 content = AddressUtils ().formatEpicCashAddress (content);
245263 }
@@ -253,6 +271,7 @@ class _SendViewState extends ConsumerState<SendView> {
253271 });
254272 }
255273 } catch (e) {
274+ _setOpReturnData (null );
256275 // strip http:// and https:// if content contains @
257276 if (coin is Epiccash ) {
258277 content = AddressUtils ().formatEpicCashAddress (content);
@@ -306,6 +325,7 @@ class _SendViewState extends ConsumerState<SendView> {
306325 paymentData.coin? .uriScheme == coin.uriScheme) {
307326 _applyUri (paymentData);
308327 } else {
328+ _setOpReturnData (null );
309329 _address = qrResult.rawContent! .split ("\n " ).first.trim ();
310330 sendToController.text = _address ?? "" ;
311331
@@ -524,11 +544,50 @@ class _SendViewState extends ConsumerState<SendView> {
524544 Map <Amount , String > cachedFiroSparkFees = {};
525545 Map <Amount , String > cachedFiroPublicFees = {};
526546
547+ void _setOpReturnData (String ? data) {
548+ if (! mounted) {
549+ return ;
550+ }
551+ ref.read (pOpReturnData.notifier).state = data;
552+ }
553+
554+ Amount _addOpReturnFeeIfNeeded ({
555+ required Amount fee,
556+ required BigInt feeRate,
557+ required FiroWallet wallet,
558+ }) {
559+ final opReturnData = ref.read (pOpReturnData);
560+ if (opReturnData == null ||
561+ opReturnData.isEmpty ||
562+ ref.read (publicPrivateBalanceStateProvider) != BalanceType .public) {
563+ return fee;
564+ }
565+
566+ final extraOutputVSize = AddressUtils .opReturnOutputVSizeFromHex (
567+ opReturnData,
568+ );
569+ final extraFee = wallet.estimateTxFee (
570+ vSize: extraOutputVSize,
571+ feeRatePerKB: feeRate,
572+ );
573+
574+ return fee +
575+ Amount (
576+ rawValue: BigInt .from (extraFee),
577+ fractionDigits: coin.fractionDigits,
578+ );
579+ }
580+
527581 Future <String > calculateFees (Amount amount) async {
582+ final hasOpReturnData =
583+ isFiro &&
584+ ref.read (publicPrivateBalanceStateProvider) == BalanceType .public &&
585+ (ref.read (pOpReturnData)? .isNotEmpty ?? false );
586+
528587 if (isFiro) {
529588 switch (ref.read (publicPrivateBalanceStateProvider.state).state) {
530589 case BalanceType .public:
531- if (cachedFiroPublicFees[amount] != null ) {
590+ if (! hasOpReturnData && cachedFiroPublicFees[amount] != null ) {
532591 return cachedFiroPublicFees[amount]! ;
533592 }
534593 break ;
@@ -590,10 +649,18 @@ class _SendViewState extends ConsumerState<SendView> {
590649 switch (ref.read (publicPrivateBalanceStateProvider.state).state) {
591650 case BalanceType .public:
592651 fee = await firoWallet.estimateFeeFor (amount, feeRate);
593- cachedFiroPublicFees[amount] = ref
652+ fee = _addOpReturnFeeIfNeeded (
653+ fee: fee,
654+ feeRate: feeRate,
655+ wallet: firoWallet,
656+ );
657+ final formatted = ref
594658 .read (pAmountFormatter (coin))
595659 .format (fee, withUnitName: true , indicatePrecisionLoss: false );
596- return cachedFiroPublicFees[amount]! ;
660+ if (! hasOpReturnData) {
661+ cachedFiroPublicFees[amount] = formatted;
662+ }
663+ return formatted;
597664
598665 case BalanceType .private:
599666 fee = await firoWallet.estimateFeeForSpark (amount);
@@ -923,6 +990,7 @@ class _SendViewState extends ConsumerState<SendView> {
923990 selectedUTXOs.isNotEmpty)
924991 ? selectedUTXOs
925992 : null ,
993+ opReturnData: ref.read (pOpReturnData),
926994 ),
927995 );
928996 } else if (wallet is FiroWallet ) {
@@ -964,6 +1032,7 @@ class _SendViewState extends ConsumerState<SendView> {
9641032 utxos: (coinControlEnabled && selectedUTXOs.isNotEmpty)
9651033 ? selectedUTXOs
9661034 : null ,
1035+ opReturnData: ref.read (pOpReturnData),
9671036 ),
9681037 );
9691038 }
@@ -1127,6 +1196,9 @@ class _SendViewState extends ConsumerState<SendView> {
11271196 }
11281197
11291198 void clearSendForm () {
1199+ if (! mounted) {
1200+ return ;
1201+ }
11301202 sendToController.text = "" ;
11311203 cryptoAmountController.text = "" ;
11321204 baseAmountController.text = "" ;
@@ -1136,9 +1208,8 @@ class _SendViewState extends ConsumerState<SendView> {
11361208 memoController.text = "" ;
11371209 _address = "" ;
11381210 _addressToggleFlag = false ;
1139- if (mounted) {
1140- setState (() {});
1141- }
1211+ _setOpReturnData (null );
1212+ setState (() {});
11421213 }
11431214
11441215 String _getSendAllTitle (
@@ -1213,6 +1284,7 @@ class _SendViewState extends ConsumerState<SendView> {
12131284 if (parsed != null ) {
12141285 _applyUri (parsed);
12151286 } else {
1287+ _setOpReturnData (null );
12161288 sendToController.text = content;
12171289 _address = content;
12181290
@@ -1726,9 +1798,10 @@ class _SendViewState extends ConsumerState<SendView> {
17261798 final trimmed = newValue.trim ();
17271799
17281800 if ((trimmed.length -
1729- (_address? .length ?? 0 ))
1730- .abs () >
1731- 1 ) {
1801+ (_address? .length ?? 0 ))
1802+ .abs () >
1803+ 1 ||
1804+ trimmed.contains (':' )) {
17321805 final parsed =
17331806 AddressUtils .parsePaymentUri (
17341807 trimmed,
@@ -1737,11 +1810,13 @@ class _SendViewState extends ConsumerState<SendView> {
17371810 if (parsed != null ) {
17381811 _applyUri (parsed);
17391812 } else {
1813+ _setOpReturnData (null );
17401814 await _checkSparkNameAndOrSetAddress (
17411815 newValue,
17421816 );
17431817 }
17441818 } else {
1819+ _setOpReturnData (null );
17451820 await _checkSparkNameAndOrSetAddress (
17461821 newValue,
17471822 setController: false ,
@@ -1791,6 +1866,9 @@ class _SendViewState extends ConsumerState<SendView> {
17911866 .text =
17921867 "" ;
17931868 _address = "" ;
1869+ _setOpReturnData (
1870+ null ,
1871+ );
17941872 _setValidAddressProviders (
17951873 _address,
17961874 );
@@ -1949,6 +2027,38 @@ class _SendViewState extends ConsumerState<SendView> {
19492027 ),
19502028 ),
19512029 ),
2030+ if (ref.watch (pOpReturnData) != null &&
2031+ _address != null &&
2032+ _address! .isNotEmpty &&
2033+ (ref.watch (pValidSendToAddress) ||
2034+ ref.watch (pValidSparkSendToAddress)) &&
2035+ balType == BalanceType .public)
2036+ Align (
2037+ alignment: Alignment .topLeft,
2038+ child: Padding (
2039+ padding: const EdgeInsets .only (
2040+ left: 12.0 ,
2041+ top: 4.0 ,
2042+ ),
2043+ child: Tooltip (
2044+ message: AddressUtils .formatOpReturnTooltip (
2045+ ref.watch (pOpReturnData)! ,
2046+ ),
2047+ child: Text (
2048+ "Transaction includes metadata "
2049+ "(${ref .watch (pOpReturnData )!.length ~/ 2 } bytes) "
2050+ "\u 2014 tap for details" ,
2051+ textAlign: TextAlign .left,
2052+ style: STextStyles .label (context)
2053+ .copyWith (
2054+ color: Theme .of (context)
2055+ .extension < StackColors > ()!
2056+ .accentColorGreen,
2057+ ),
2058+ ),
2059+ ),
2060+ ),
2061+ ),
19522062 Builder (
19532063 builder: (_) {
19542064 final String ? error;
@@ -2666,16 +2776,42 @@ class _SendViewState extends ConsumerState<SendView> {
26662776 ),
26672777 const Spacer (),
26682778 const SizedBox (height: 12 ),
2779+ if (ref.watch (pOpReturnData) != null &&
2780+ balType == BalanceType .private)
2781+ Padding (
2782+ padding: const EdgeInsets .only (
2783+ left: 12.0 ,
2784+ right: 12.0 ,
2785+ bottom: 12.0 ,
2786+ ),
2787+ child: Text (
2788+ "Bridge data detected but Spark (private) "
2789+ "transactions cannot carry OP_RETURN data. "
2790+ "Switch to public balance to complete the "
2791+ "bridge transaction." ,
2792+ textAlign: TextAlign .left,
2793+ style: STextStyles .label (context).copyWith (
2794+ color: Theme .of (
2795+ context,
2796+ ).extension < StackColors > ()! .textError,
2797+ ),
2798+ ),
2799+ ),
26692800 TextButton (
26702801 onPressed:
2671- ref.watch (pPreviewTxButtonEnabled (coin))
2802+ ref.watch (pPreviewTxButtonEnabled (coin)) &&
2803+ (ref.watch (pOpReturnData) == null ||
2804+ balType != BalanceType .private)
26722805 ? isMwcSlatepack
26732806 ? _createSlatepack
26742807 : isEpicSlatepack
26752808 ? _createEpicSlatepack
26762809 : _previewTransaction
26772810 : null ,
2678- style: ref.watch (pPreviewTxButtonEnabled (coin))
2811+ style:
2812+ ref.watch (pPreviewTxButtonEnabled (coin)) &&
2813+ (ref.watch (pOpReturnData) == null ||
2814+ balType != BalanceType .private)
26792815 ? Theme .of (context)
26802816 .extension < StackColors > ()!
26812817 .getPrimaryEnabledButtonStyle (context)
0 commit comments