Skip to content

Commit 4fa4ee8

Browse files
committed
Add OP_RETURN support required for rosen bridge
1 parent 2a37aeb commit 4fa4ee8

6 files changed

Lines changed: 328 additions & 43 deletions

File tree

lib/pages/send_view/send_view.dart

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ class _SendViewState extends ConsumerState<SendView> {
159159
try {
160160
// auto fill address
161161
_address = paymentData.address.trim();
162-
sendToController.text = _address!;
163162

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

181+
// Extract OP_RETURN data if present (for Rosen Bridge and other protocols)
182+
// Must be set BEFORE sendToController.text to avoid re-entrant
183+
// onChanged handler reading stale null value.
184+
if (paymentData.additionalParams.containsKey('op_return')) {
185+
final data = paymentData.additionalParams['op_return'];
186+
ref.read(pOpReturnData.notifier).state = data;
187+
Logging.instance.i(
188+
"Extracted OP_RETURN data from URI, length: ${data!.length ~/ 2} bytes",
189+
);
190+
} else {
191+
ref.read(pOpReturnData.notifier).state = null;
192+
}
193+
182194
_setValidAddressProviders(_address);
195+
196+
// Assign controller.text last — it triggers onChanged which depends
197+
// on pOpReturnData already being set above.
198+
sendToController.text = _address!;
199+
183200
setState(() {
184201
_addressToggleFlag = sendToController.text.isNotEmpty;
185202
});
@@ -919,6 +936,7 @@ class _SendViewState extends ConsumerState<SendView> {
919936
selectedUTXOs.isNotEmpty)
920937
? selectedUTXOs
921938
: null,
939+
opReturnData: ref.read(pOpReturnData),
922940
),
923941
);
924942
} else if (wallet is FiroWallet) {
@@ -960,6 +978,7 @@ class _SendViewState extends ConsumerState<SendView> {
960978
utxos: (coinControlEnabled && selectedUTXOs.isNotEmpty)
961979
? selectedUTXOs
962980
: null,
981+
opReturnData: ref.read(pOpReturnData),
963982
),
964983
);
965984
}
@@ -1131,6 +1150,7 @@ class _SendViewState extends ConsumerState<SendView> {
11311150
memoController.text = "";
11321151
_address = "";
11331152
_addressToggleFlag = false;
1153+
ref.read(pOpReturnData.notifier).state = null;
11341154
if (mounted) {
11351155
setState(() {});
11361156
}
@@ -1720,9 +1740,10 @@ class _SendViewState extends ConsumerState<SendView> {
17201740
final trimmed = newValue.trim();
17211741

17221742
if ((trimmed.length -
1723-
(_address?.length ?? 0))
1724-
.abs() >
1725-
1) {
1743+
(_address?.length ?? 0))
1744+
.abs() >
1745+
1 ||
1746+
trimmed.contains(':')) {
17261747
final parsed =
17271748
AddressUtils.parsePaymentUri(
17281749
trimmed,
@@ -1731,6 +1752,8 @@ class _SendViewState extends ConsumerState<SendView> {
17311752
if (parsed != null) {
17321753
_applyUri(parsed);
17331754
} else {
1755+
ref.read(pOpReturnData.notifier).state =
1756+
null;
17341757
await _checkSparkNameAndOrSetAddress(
17351758
newValue,
17361759
);
@@ -1943,6 +1966,38 @@ class _SendViewState extends ConsumerState<SendView> {
19431966
),
19441967
),
19451968
),
1969+
if (ref.watch(pOpReturnData) != null &&
1970+
_address != null &&
1971+
_address!.isNotEmpty &&
1972+
(ref.watch(pValidSendToAddress) ||
1973+
ref.watch(pValidSparkSendToAddress)) &&
1974+
balType == BalanceType.public)
1975+
Align(
1976+
alignment: Alignment.topLeft,
1977+
child: Padding(
1978+
padding: const EdgeInsets.only(
1979+
left: 12.0,
1980+
top: 4.0,
1981+
),
1982+
child: Tooltip(
1983+
message: AddressUtils.formatOpReturnTooltip(
1984+
ref.watch(pOpReturnData)!,
1985+
),
1986+
child: Text(
1987+
"Transaction includes metadata "
1988+
"(${ref.watch(pOpReturnData)!.length ~/ 2} bytes) "
1989+
"\u2014 tap for details",
1990+
textAlign: TextAlign.left,
1991+
style: STextStyles.label(context)
1992+
.copyWith(
1993+
color: Theme.of(context)
1994+
.extension<StackColors>()!
1995+
.accentColorGreen,
1996+
),
1997+
),
1998+
),
1999+
),
2000+
),
19462001
Builder(
19472002
builder: (_) {
19482003
final String? error;
@@ -2660,16 +2715,42 @@ class _SendViewState extends ConsumerState<SendView> {
26602715
),
26612716
const Spacer(),
26622717
const SizedBox(height: 12),
2718+
if (ref.watch(pOpReturnData) != null &&
2719+
balType == BalanceType.private)
2720+
Padding(
2721+
padding: const EdgeInsets.only(
2722+
left: 12.0,
2723+
right: 12.0,
2724+
bottom: 12.0,
2725+
),
2726+
child: Text(
2727+
"Bridge data detected but Spark (private) "
2728+
"transactions cannot carry OP_RETURN data. "
2729+
"Switch to public balance to complete the "
2730+
"bridge transaction.",
2731+
textAlign: TextAlign.left,
2732+
style: STextStyles.label(context).copyWith(
2733+
color: Theme.of(
2734+
context,
2735+
).extension<StackColors>()!.textError,
2736+
),
2737+
),
2738+
),
26632739
TextButton(
26642740
onPressed:
2665-
ref.watch(pPreviewTxButtonEnabled(coin))
2741+
ref.watch(pPreviewTxButtonEnabled(coin)) &&
2742+
(ref.watch(pOpReturnData) == null ||
2743+
balType != BalanceType.private)
26662744
? isMwcSlatepack
26672745
? _createSlatepack
26682746
: isEpicSlatepack
26692747
? _createEpicSlatepack
26702748
: _previewTransaction
26712749
: null,
2672-
style: ref.watch(pPreviewTxButtonEnabled(coin))
2750+
style:
2751+
ref.watch(pPreviewTxButtonEnabled(coin)) &&
2752+
(ref.watch(pOpReturnData) == null ||
2753+
balType != BalanceType.private)
26732754
? Theme.of(context)
26742755
.extension<StackColors>()!
26752756
.getPrimaryEnabledButtonStyle(context)

lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
646646
ref.read(pDesktopUseUTXOs).isNotEmpty)
647647
? ref.read(pDesktopUseUTXOs)
648648
: null,
649+
opReturnData: ref.read(pOpReturnData),
649650
),
650651
);
651652
}
@@ -915,8 +916,11 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
915916

916917
if (paymentData != null &&
917918
paymentData.coin?.uriScheme == coin.uriScheme) {
919+
ref.read(pOpReturnData.notifier).state =
920+
paymentData.additionalParams['op_return'];
918921
_applyUri(paymentData);
919922
} else {
923+
ref.read(pOpReturnData.notifier).state = null;
920924
_address = qrCodeData.split("\n").first.trim();
921925
sendToController.text = _address ?? "";
922926

@@ -1045,8 +1049,11 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
10451049
);
10461050
if (paymentData != null &&
10471051
paymentData.coin?.uriScheme == coin.uriScheme) {
1052+
ref.read(pOpReturnData.notifier).state =
1053+
paymentData.additionalParams['op_return'];
10481054
_applyUri(paymentData);
10491055
} else {
1056+
ref.read(pOpReturnData.notifier).state = null;
10501057
if (coin is Epiccash) {
10511058
content = AddressUtils().formatEpicCashAddress(content);
10521059
}
@@ -1063,6 +1070,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
10631070
});
10641071
}
10651072
} catch (e) {
1073+
ref.read(pOpReturnData.notifier).state = null;
10661074
// If parsing fails, treat it as a plain address.
10671075
if (coin is Epiccash) {
10681076
// strip http:// and https:// if content contains @
@@ -1748,14 +1756,18 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
17481756
onChanged: (newValue) async {
17491757
final trimmed = newValue;
17501758

1751-
if ((trimmed.length - (_address?.length ?? 0)).abs() > 1) {
1759+
if ((trimmed.length - (_address?.length ?? 0)).abs() > 1 ||
1760+
trimmed.contains(':')) {
17521761
final parsed = AddressUtils.parsePaymentUri(
17531762
trimmed,
17541763
logging: Logging.instance,
17551764
);
17561765
if (parsed != null) {
1766+
ref.read(pOpReturnData.notifier).state =
1767+
parsed.additionalParams['op_return'];
17571768
_applyUri(parsed);
17581769
} else {
1770+
ref.read(pOpReturnData.notifier).state = null;
17591771
await _checkSparkNameAndOrSetAddress(newValue);
17601772
}
17611773
} else {
@@ -1809,6 +1821,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
18091821
onTap: () {
18101822
sendToController.text = "";
18111823
_address = "";
1824+
ref.read(pOpReturnData.notifier).state =
1825+
null;
18121826
_setValidAddressProviders(_address);
18131827
setState(() {
18141828
_addressToggleFlag = false;
@@ -1954,6 +1968,66 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
19541968
}
19551969
},
19561970
),
1971+
// OP_RETURN metadata info (green, public mode only, with tooltip)
1972+
Builder(
1973+
builder: (context) {
1974+
final opData = ref.watch(pOpReturnData);
1975+
final balType = ref.watch(publicPrivateBalanceStateProvider);
1976+
if (opData == null ||
1977+
opData.isEmpty ||
1978+
balType != BalanceType.public) {
1979+
return Container();
1980+
}
1981+
return Align(
1982+
alignment: Alignment.topLeft,
1983+
child: Padding(
1984+
padding: const EdgeInsets.only(left: 12.0, top: 4.0),
1985+
child: Tooltip(
1986+
message: AddressUtils.formatOpReturnTooltip(opData),
1987+
child: Text(
1988+
"Transaction includes metadata "
1989+
"(${opData.length ~/ 2} bytes)",
1990+
textAlign: TextAlign.left,
1991+
style: STextStyles.label(context).copyWith(
1992+
color: Theme.of(
1993+
context,
1994+
).extension<StackColors>()!.accentColorGreen,
1995+
),
1996+
),
1997+
),
1998+
),
1999+
);
2000+
},
2001+
),
2002+
// OP_RETURN bridge warning (red, private mode only)
2003+
Builder(
2004+
builder: (context) {
2005+
final opData = ref.watch(pOpReturnData);
2006+
final balType = ref.watch(publicPrivateBalanceStateProvider);
2007+
if (opData == null ||
2008+
opData.isEmpty ||
2009+
balType != BalanceType.private) {
2010+
return Container();
2011+
}
2012+
return Align(
2013+
alignment: Alignment.topLeft,
2014+
child: Padding(
2015+
padding: const EdgeInsets.only(left: 12.0, top: 4.0),
2016+
child: Text(
2017+
"Bridge data detected but Spark (private) transactions "
2018+
"cannot carry OP_RETURN data. Switch to public balance "
2019+
"to complete the bridge transaction.",
2020+
textAlign: TextAlign.left,
2021+
style: STextStyles.label(context).copyWith(
2022+
color: Theme.of(
2023+
context,
2024+
).extension<StackColors>()!.textError,
2025+
),
2026+
),
2027+
),
2028+
);
2029+
},
2030+
),
19572031
if (hasOptionalMemo || ref.watch(pValidSparkSendToAddress))
19582032
const SizedBox(height: 10),
19592033
if (hasOptionalMemo || ref.watch(pValidSparkSendToAddress))

lib/providers/ui/preview_tx_button_state_provider.dart

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ final pValidSparkSendToAddress = StateProvider.autoDispose<bool>((_) => false);
2323

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

26+
final pOpReturnData = StateProvider<String?>((_) => null);
27+
2628
// MWC Transaction Method Provider.
2729
final pSelectedMwcTransactionMethod = StateProvider<MwcTransactionMethod>(
2830
(_) => MwcTransactionMethod.slatepack,
@@ -47,42 +49,44 @@ final pIsSlatepack = Provider.family<bool, String>((ref, walletId) {
4749
return false;
4850
});
4951

50-
final pPreviewTxButtonEnabled = Provider.autoDispose
51-
.family<bool, CryptoCurrency>((ref, coin) {
52-
final amount = ref.watch(pSendAmount) ?? Amount.zero;
52+
final pPreviewTxButtonEnabled = Provider.autoDispose.family<bool, CryptoCurrency>(
53+
(ref, coin) {
54+
final amount = ref.watch(pSendAmount) ?? Amount.zero;
5355

54-
// For MWC slatepack transactions, address validation is not required.
55-
if (coin is Mimblewimblecoin) {
56-
final selectedMethod = ref.watch(pSelectedMwcTransactionMethod);
57-
if (selectedMethod == MwcTransactionMethod.slatepack) {
58-
return amount > Amount.zero;
59-
}
56+
// For MWC slatepack transactions, address validation is not required.
57+
if (coin is Mimblewimblecoin) {
58+
final selectedMethod = ref.watch(pSelectedMwcTransactionMethod);
59+
if (selectedMethod == MwcTransactionMethod.slatepack) {
60+
return amount > Amount.zero;
6061
}
62+
}
6163

62-
// For Epic Cash slatepack transactions, address validation is not required.
63-
if (coin is Epiccash) {
64-
final selectedMethod = ref.watch(pSelectedEpicTransactionMethod);
65-
if (selectedMethod == EpicTransactionMethod.slatepack) {
66-
return amount > Amount.zero;
67-
}
64+
// For Epic Cash slatepack transactions, address validation is not required.
65+
if (coin is Epiccash) {
66+
final selectedMethod = ref.watch(pSelectedEpicTransactionMethod);
67+
if (selectedMethod == EpicTransactionMethod.slatepack) {
68+
return amount > Amount.zero;
6869
}
70+
}
6971

70-
if (coin is Firo) {
71-
final firoType = ref.watch(publicPrivateBalanceStateProvider);
72-
switch (firoType) {
73-
case BalanceType.private:
74-
return (ref.watch(pValidSendToAddress) ||
75-
ref.watch(pValidSparkSendToAddress)) &&
76-
!ref.watch(pIsExchangeAddress) &&
77-
amount > Amount.zero;
72+
if (coin is Firo) {
73+
final firoType = ref.watch(publicPrivateBalanceStateProvider);
74+
switch (firoType) {
75+
case BalanceType.private:
76+
return (ref.watch(pValidSendToAddress) ||
77+
ref.watch(pValidSparkSendToAddress)) &&
78+
!ref.watch(pIsExchangeAddress) &&
79+
ref.watch(pOpReturnData) == null &&
80+
amount > Amount.zero;
7881

79-
case BalanceType.public:
80-
return ref.watch(pValidSendToAddress) && amount > Amount.zero;
81-
}
82-
} else {
83-
return ref.watch(pValidSendToAddress) && amount > Amount.zero;
82+
case BalanceType.public:
83+
return ref.watch(pValidSendToAddress) && amount > Amount.zero;
8484
}
85-
});
85+
} else {
86+
return ref.watch(pValidSendToAddress) && amount > Amount.zero;
87+
}
88+
},
89+
);
8690

8791
final previewTokenTxButtonStateProvider = StateProvider.autoDispose<bool>((_) {
8892
return false;

0 commit comments

Comments
 (0)