Skip to content

Commit c9035e4

Browse files
committed
Add OP_RETURN support required for rosen bridge
1 parent 76cc531 commit c9035e4

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
@@ -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+
ref.read(pOpReturnData.notifier).state = data;
188+
Logging.instance.i(
189+
"Extracted OP_RETURN data from URI, length: ${data!.length ~/ 2} bytes",
190+
);
191+
} else {
192+
ref.read(pOpReturnData.notifier).state = 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
});
@@ -923,6 +940,7 @@ class _SendViewState extends ConsumerState<SendView> {
923940
selectedUTXOs.isNotEmpty)
924941
? selectedUTXOs
925942
: null,
943+
opReturnData: ref.read(pOpReturnData),
926944
),
927945
);
928946
} else if (wallet is FiroWallet) {
@@ -964,6 +982,7 @@ class _SendViewState extends ConsumerState<SendView> {
964982
utxos: (coinControlEnabled && selectedUTXOs.isNotEmpty)
965983
? selectedUTXOs
966984
: null,
985+
opReturnData: ref.read(pOpReturnData),
967986
),
968987
);
969988
}
@@ -1136,6 +1155,7 @@ class _SendViewState extends ConsumerState<SendView> {
11361155
memoController.text = "";
11371156
_address = "";
11381157
_addressToggleFlag = false;
1158+
ref.read(pOpReturnData.notifier).state = null;
11391159
if (mounted) {
11401160
setState(() {});
11411161
}
@@ -1726,9 +1746,10 @@ class _SendViewState extends ConsumerState<SendView> {
17261746
final trimmed = newValue.trim();
17271747

17281748
if ((trimmed.length -
1729-
(_address?.length ?? 0))
1730-
.abs() >
1731-
1) {
1749+
(_address?.length ?? 0))
1750+
.abs() >
1751+
1 ||
1752+
trimmed.contains(':')) {
17321753
final parsed =
17331754
AddressUtils.parsePaymentUri(
17341755
trimmed,
@@ -1737,6 +1758,8 @@ class _SendViewState extends ConsumerState<SendView> {
17371758
if (parsed != null) {
17381759
_applyUri(parsed);
17391760
} else {
1761+
ref.read(pOpReturnData.notifier).state =
1762+
null;
17401763
await _checkSparkNameAndOrSetAddress(
17411764
newValue,
17421765
);
@@ -1949,6 +1972,38 @@ class _SendViewState extends ConsumerState<SendView> {
19491972
),
19501973
),
19511974
),
1975+
if (ref.watch(pOpReturnData) != null &&
1976+
_address != null &&
1977+
_address!.isNotEmpty &&
1978+
(ref.watch(pValidSendToAddress) ||
1979+
ref.watch(pValidSparkSendToAddress)) &&
1980+
balType == BalanceType.public)
1981+
Align(
1982+
alignment: Alignment.topLeft,
1983+
child: Padding(
1984+
padding: const EdgeInsets.only(
1985+
left: 12.0,
1986+
top: 4.0,
1987+
),
1988+
child: Tooltip(
1989+
message: AddressUtils.formatOpReturnTooltip(
1990+
ref.watch(pOpReturnData)!,
1991+
),
1992+
child: Text(
1993+
"Transaction includes metadata "
1994+
"(${ref.watch(pOpReturnData)!.length ~/ 2} bytes) "
1995+
"\u2014 tap for details",
1996+
textAlign: TextAlign.left,
1997+
style: STextStyles.label(context)
1998+
.copyWith(
1999+
color: Theme.of(context)
2000+
.extension<StackColors>()!
2001+
.accentColorGreen,
2002+
),
2003+
),
2004+
),
2005+
),
2006+
),
19522007
Builder(
19532008
builder: (_) {
19542009
final String? error;
@@ -2666,16 +2721,42 @@ class _SendViewState extends ConsumerState<SendView> {
26662721
),
26672722
const Spacer(),
26682723
const SizedBox(height: 12),
2724+
if (ref.watch(pOpReturnData) != null &&
2725+
balType == BalanceType.private)
2726+
Padding(
2727+
padding: const EdgeInsets.only(
2728+
left: 12.0,
2729+
right: 12.0,
2730+
bottom: 12.0,
2731+
),
2732+
child: Text(
2733+
"Bridge data detected but Spark (private) "
2734+
"transactions cannot carry OP_RETURN data. "
2735+
"Switch to public balance to complete the "
2736+
"bridge transaction.",
2737+
textAlign: TextAlign.left,
2738+
style: STextStyles.label(context).copyWith(
2739+
color: Theme.of(
2740+
context,
2741+
).extension<StackColors>()!.textError,
2742+
),
2743+
),
2744+
),
26692745
TextButton(
26702746
onPressed:
2671-
ref.watch(pPreviewTxButtonEnabled(coin))
2747+
ref.watch(pPreviewTxButtonEnabled(coin)) &&
2748+
(ref.watch(pOpReturnData) == null ||
2749+
balType != BalanceType.private)
26722750
? isMwcSlatepack
26732751
? _createSlatepack
26742752
: isEpicSlatepack
26752753
? _createEpicSlatepack
26762754
: _previewTransaction
26772755
: null,
2678-
style: ref.watch(pPreviewTxButtonEnabled(coin))
2756+
style:
2757+
ref.watch(pPreviewTxButtonEnabled(coin)) &&
2758+
(ref.watch(pOpReturnData) == null ||
2759+
balType != BalanceType.private)
26792760
? Theme.of(context)
26802761
.extension<StackColors>()!
26812762
.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
@@ -650,6 +650,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
650650
ref.read(pDesktopUseUTXOs).isNotEmpty)
651651
? ref.read(pDesktopUseUTXOs)
652652
: null,
653+
opReturnData: ref.read(pOpReturnData),
653654
),
654655
);
655656
}
@@ -920,8 +921,11 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
920921

921922
if (paymentData != null &&
922923
paymentData.coin?.uriScheme == coin.uriScheme) {
924+
ref.read(pOpReturnData.notifier).state =
925+
paymentData.additionalParams['op_return'];
923926
_applyUri(paymentData);
924927
} else {
928+
ref.read(pOpReturnData.notifier).state = null;
925929
_address = qrCodeData.split("\n").first.trim();
926930
sendToController.text = _address ?? "";
927931

@@ -1050,8 +1054,11 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
10501054
);
10511055
if (paymentData != null &&
10521056
paymentData.coin?.uriScheme == coin.uriScheme) {
1057+
ref.read(pOpReturnData.notifier).state =
1058+
paymentData.additionalParams['op_return'];
10531059
_applyUri(paymentData);
10541060
} else {
1061+
ref.read(pOpReturnData.notifier).state = null;
10551062
if (coin is Epiccash) {
10561063
content = AddressUtils().formatEpicCashAddress(content);
10571064
}
@@ -1068,6 +1075,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
10681075
});
10691076
}
10701077
} catch (e) {
1078+
ref.read(pOpReturnData.notifier).state = null;
10711079
// If parsing fails, treat it as a plain address.
10721080
if (coin is Epiccash) {
10731081
// strip http:// and https:// if content contains @
@@ -1754,14 +1762,18 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
17541762
onChanged: (newValue) async {
17551763
final trimmed = newValue;
17561764

1757-
if ((trimmed.length - (_address?.length ?? 0)).abs() > 1) {
1765+
if ((trimmed.length - (_address?.length ?? 0)).abs() > 1 ||
1766+
trimmed.contains(':')) {
17581767
final parsed = AddressUtils.parsePaymentUri(
17591768
trimmed,
17601769
logging: Logging.instance,
17611770
);
17621771
if (parsed != null) {
1772+
ref.read(pOpReturnData.notifier).state =
1773+
parsed.additionalParams['op_return'];
17631774
_applyUri(parsed);
17641775
} else {
1776+
ref.read(pOpReturnData.notifier).state = null;
17651777
await _checkSparkNameAndOrSetAddress(newValue);
17661778
}
17671779
} else {
@@ -1815,6 +1827,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
18151827
onTap: () {
18161828
sendToController.text = "";
18171829
_address = "";
1830+
ref.read(pOpReturnData.notifier).state =
1831+
null;
18181832
_setValidAddressProviders(_address);
18191833
setState(() {
18201834
_addressToggleFlag = false;
@@ -1960,6 +1974,66 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
19601974
}
19611975
},
19621976
),
1977+
// OP_RETURN metadata info (green, public mode only, with tooltip)
1978+
Builder(
1979+
builder: (context) {
1980+
final opData = ref.watch(pOpReturnData);
1981+
final balType = ref.watch(publicPrivateBalanceStateProvider);
1982+
if (opData == null ||
1983+
opData.isEmpty ||
1984+
balType != BalanceType.public) {
1985+
return Container();
1986+
}
1987+
return Align(
1988+
alignment: Alignment.topLeft,
1989+
child: Padding(
1990+
padding: const EdgeInsets.only(left: 12.0, top: 4.0),
1991+
child: Tooltip(
1992+
message: AddressUtils.formatOpReturnTooltip(opData),
1993+
child: Text(
1994+
"Transaction includes metadata "
1995+
"(${opData.length ~/ 2} bytes)",
1996+
textAlign: TextAlign.left,
1997+
style: STextStyles.label(context).copyWith(
1998+
color: Theme.of(
1999+
context,
2000+
).extension<StackColors>()!.accentColorGreen,
2001+
),
2002+
),
2003+
),
2004+
),
2005+
);
2006+
},
2007+
),
2008+
// OP_RETURN bridge warning (red, private mode only)
2009+
Builder(
2010+
builder: (context) {
2011+
final opData = ref.watch(pOpReturnData);
2012+
final balType = ref.watch(publicPrivateBalanceStateProvider);
2013+
if (opData == null ||
2014+
opData.isEmpty ||
2015+
balType != BalanceType.private) {
2016+
return Container();
2017+
}
2018+
return Align(
2019+
alignment: Alignment.topLeft,
2020+
child: Padding(
2021+
padding: const EdgeInsets.only(left: 12.0, top: 4.0),
2022+
child: Text(
2023+
"Bridge data detected but Spark (private) transactions "
2024+
"cannot carry OP_RETURN data. Switch to public balance "
2025+
"to complete the bridge transaction.",
2026+
textAlign: TextAlign.left,
2027+
style: STextStyles.label(context).copyWith(
2028+
color: Theme.of(
2029+
context,
2030+
).extension<StackColors>()!.textError,
2031+
),
2032+
),
2033+
),
2034+
);
2035+
},
2036+
),
19632037
if (hasOptionalMemo || ref.watch(pValidSparkSendToAddress))
19642038
const SizedBox(height: 10),
19652039
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)