Skip to content

Commit 8d4ff98

Browse files
committed
Add OP_RETURN support required for rosen bridge
1 parent cb8a465 commit 8d4ff98

4 files changed

Lines changed: 153 additions & 9 deletions

File tree

lib/pages/send_view/send_view.dart

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ class _SendViewState extends ConsumerState<SendView> {
145145

146146
Amount? _cachedAmountToSend;
147147
String? _address;
148+
String? _opReturnData;
148149

149150
bool _addressToggleFlag = false;
150151

@@ -179,6 +180,16 @@ class _SendViewState extends ConsumerState<SendView> {
179180
ref.read(pSendAmount.notifier).state = amount;
180181
}
181182

183+
// Extract OP_RETURN data if present (for Rosen Bridge and other protocols)
184+
if (paymentData.additionalParams.containsKey('op_return')) {
185+
_opReturnData = paymentData.additionalParams['op_return'];
186+
Logging.instance.i(
187+
"Extracted OP_RETURN data from URI, length: ${_opReturnData!.length ~/ 2} bytes",
188+
);
189+
} else {
190+
_opReturnData = null;
191+
}
192+
182193
_setValidAddressProviders(_address);
183194
setState(() {
184195
_addressToggleFlag = sendToController.text.isNotEmpty;
@@ -919,6 +930,7 @@ class _SendViewState extends ConsumerState<SendView> {
919930
selectedUTXOs.isNotEmpty)
920931
? selectedUTXOs
921932
: null,
933+
opReturnData: _opReturnData,
922934
),
923935
);
924936
} else if (wallet is FiroWallet) {
@@ -960,6 +972,7 @@ class _SendViewState extends ConsumerState<SendView> {
960972
utxos: (coinControlEnabled && selectedUTXOs.isNotEmpty)
961973
? selectedUTXOs
962974
: null,
975+
opReturnData: _opReturnData,
963976
),
964977
);
965978
}
@@ -1041,6 +1054,7 @@ class _SendViewState extends ConsumerState<SendView> {
10411054
selectedUTXOs.isNotEmpty)
10421055
? selectedUTXOs
10431056
: null,
1057+
opReturnData: _opReturnData,
10441058
),
10451059
);
10461060
}
@@ -1131,6 +1145,7 @@ class _SendViewState extends ConsumerState<SendView> {
11311145
memoController.text = "";
11321146
_address = "";
11331147
_addressToggleFlag = false;
1148+
_opReturnData = null;
11341149
if (mounted) {
11351150
setState(() {});
11361151
}
@@ -1943,6 +1958,36 @@ class _SendViewState extends ConsumerState<SendView> {
19431958
),
19441959
),
19451960
),
1961+
if (_opReturnData != null &&
1962+
_opReturnData!.isNotEmpty &&
1963+
_address != null &&
1964+
_address!.isNotEmpty &&
1965+
(ref.watch(pValidSendToAddress) ||
1966+
ref.watch(pValidSparkSendToAddress)))
1967+
Align(
1968+
alignment: Alignment.topLeft,
1969+
child: Padding(
1970+
padding: const EdgeInsets.only(
1971+
left: 12.0,
1972+
top: 4.0,
1973+
),
1974+
child: Tooltip(
1975+
message: _opReturnData,
1976+
child: Text(
1977+
"Transaction metadata "
1978+
"(${_opReturnData!.length ~/ 2} bytes) "
1979+
"\u2014 tap for details",
1980+
textAlign: TextAlign.left,
1981+
style: STextStyles.label(context)
1982+
.copyWith(
1983+
color: Theme.of(context)
1984+
.extension<StackColors>()!
1985+
.accentColorBlue,
1986+
),
1987+
),
1988+
),
1989+
),
1990+
),
19461991
Builder(
19471992
builder: (_) {
19481993
final String? error;
@@ -2660,16 +2705,42 @@ class _SendViewState extends ConsumerState<SendView> {
26602705
),
26612706
const Spacer(),
26622707
const SizedBox(height: 12),
2708+
if (_opReturnData != null &&
2709+
balType == BalanceType.private)
2710+
Padding(
2711+
padding: const EdgeInsets.only(
2712+
left: 12.0,
2713+
right: 12.0,
2714+
bottom: 12.0,
2715+
),
2716+
child: Text(
2717+
"Bridge data detected but Spark (private) "
2718+
"transactions cannot carry OP_RETURN data. "
2719+
"Switch to public balance to complete the "
2720+
"bridge transaction.",
2721+
textAlign: TextAlign.left,
2722+
style: STextStyles.label(context).copyWith(
2723+
color: Theme.of(
2724+
context,
2725+
).extension<StackColors>()!.textError,
2726+
),
2727+
),
2728+
),
26632729
TextButton(
26642730
onPressed:
2665-
ref.watch(pPreviewTxButtonEnabled(coin))
2731+
ref.watch(pPreviewTxButtonEnabled(coin)) &&
2732+
(_opReturnData == null ||
2733+
balType != BalanceType.private)
26662734
? isMwcSlatepack
26672735
? _createSlatepack
26682736
: isEpicSlatepack
26692737
? _createEpicSlatepack
26702738
: _previewTransaction
26712739
: null,
2672-
style: ref.watch(pPreviewTxButtonEnabled(coin))
2740+
style:
2741+
ref.watch(pPreviewTxButtonEnabled(coin)) &&
2742+
(_opReturnData == null ||
2743+
balType != BalanceType.private)
26732744
? Theme.of(context)
26742745
.extension<StackColors>()!
26752746
.getPrimaryEnabledButtonStyle(context)

lib/utilities/address_utils.dart

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class AddressUtils {
2323
'tx_payment_id',
2424
'recipient_name',
2525
'tx_description',
26+
'op_return', // For Rosen Bridge and other OP_RETURN protocols.
2627
// TODO [prio=med]: Add more recognized params for other coins.
2728
};
2829

@@ -268,21 +269,29 @@ class AddressUtils {
268269
if ((mimblewimblecoinAddress.startsWith("http://") ||
269270
mimblewimblecoinAddress.startsWith("https://")) &&
270271
mimblewimblecoinAddress.contains("@")) {
271-
mimblewimblecoinAddress =
272-
mimblewimblecoinAddress.replaceAll("http://", "");
273-
mimblewimblecoinAddress =
274-
mimblewimblecoinAddress.replaceAll("https://", "");
272+
mimblewimblecoinAddress = mimblewimblecoinAddress.replaceAll(
273+
"http://",
274+
"",
275+
);
276+
mimblewimblecoinAddress = mimblewimblecoinAddress.replaceAll(
277+
"https://",
278+
"",
279+
);
275280
}
276281
// strip mailto: prefix
277282
if (mimblewimblecoinAddress.startsWith("mailto:")) {
278-
mimblewimblecoinAddress =
279-
mimblewimblecoinAddress.replaceAll("mailto:", "");
283+
mimblewimblecoinAddress = mimblewimblecoinAddress.replaceAll(
284+
"mailto:",
285+
"",
286+
);
280287
}
281288
// strip / suffix if the address contains an @ symbol (and is thus an mwcmqs address)
282289
if (mimblewimblecoinAddress.endsWith("/") &&
283290
mimblewimblecoinAddress.contains("@")) {
284291
mimblewimblecoinAddress = mimblewimblecoinAddress.substring(
285-
0, mimblewimblecoinAddress.length - 1);
292+
0,
293+
mimblewimblecoinAddress.length - 1,
294+
);
286295
}
287296
return mimblewimblecoinAddress;
288297
}

lib/wallets/models/tx_data.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ class TxData {
112112

113113
final bool salviumStakeTx;
114114

115+
// Generic OP_RETURN data (hex string) - for Rosen Bridge and other protocols
116+
final String? opReturnData;
117+
115118
TxData({
116119
this.feeRateType,
117120
this.feeRateAmount,
@@ -149,6 +152,7 @@ class TxData {
149152
this.sparkNameInfo,
150153
this.vExtraData,
151154
this.overrideVersion,
155+
this.opReturnData,
152156
this.type = TxType.regular,
153157
this.salviumStakeTx = false,
154158
});
@@ -263,6 +267,7 @@ class TxData {
263267
String? noteOnChain,
264268
String? memo,
265269
String? otherData,
270+
String? opReturnData,
266271
Set<BaseInput>? utxos,
267272
List<BaseInput>? usedUTXOs,
268273
List<TxRecipient>? recipients,
@@ -341,6 +346,7 @@ class TxData {
341346
sparkNameInfo: sparkNameInfo ?? this.sparkNameInfo,
342347
vExtraData: vExtraData ?? this.vExtraData,
343348
overrideVersion: overrideVersion ?? this.overrideVersion,
349+
opReturnData: opReturnData ?? this.opReturnData,
344350
type: type ?? this.type,
345351
);
346352
}
@@ -383,6 +389,7 @@ class TxData {
383389
'sparkNameInfo: $sparkNameInfo, '
384390
'vExtraData: ${vExtraData?.toHex}, '
385391
'overrideVersion: $overrideVersion, '
392+
'opReturnData: $opReturnData, '
386393
'type: $type, '
387394
'}';
388395
}

lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,63 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
852852
);
853853
}
854854

855+
// Add OP_RETURN output if provided (for Rosen Bridge and other protocols)
856+
// Currently only supported for Firo
857+
if (cryptoCurrency is Firo &&
858+
txData.opReturnData != null &&
859+
txData.opReturnData!.isNotEmpty) {
860+
try {
861+
final opReturnBytes = txData.opReturnData!.toUint8ListFromHex;
862+
863+
// Validate OP_RETURN size (Bitcoin/Firo limit is 80 bytes)
864+
if (opReturnBytes.length > 80) {
865+
throw Exception(
866+
"OP_RETURN data exceeds 80 byte limit: ${opReturnBytes.length} bytes",
867+
);
868+
}
869+
870+
// Encode push data: OP_PUSHDATA1 (0x4c) for 76-80 bytes, direct length otherwise
871+
final pushData = opReturnBytes.length <= 75
872+
? Uint8List.fromList([opReturnBytes.length, ...opReturnBytes])
873+
: Uint8List.fromList([
874+
0x4c,
875+
opReturnBytes.length,
876+
...opReturnBytes,
877+
]);
878+
879+
final opReturnScript = Uint8List.fromList([
880+
0x6a, // OP_RETURN opcode
881+
...pushData,
882+
]);
883+
884+
final opReturnOutput = coinlib.Output.fromScriptBytes(
885+
BigInt.zero, // OP_RETURN outputs have 0 value
886+
opReturnScript,
887+
);
888+
889+
clTx = clTx.addOutput(opReturnOutput);
890+
891+
Logging.instance.i(
892+
"Added OP_RETURN output with ${opReturnBytes.length} bytes of data",
893+
);
894+
895+
tempOutputs.add(
896+
OutputV2.isarCantDoRequiredInDefaultConstructor(
897+
scriptPubKeyHex: opReturnScript.toHex,
898+
valueStringSats: "0",
899+
addresses: [],
900+
walletOwns: false,
901+
),
902+
);
903+
} catch (e, s) {
904+
Logging.instance.e(
905+
"Failed to add OP_RETURN output",
906+
error: e,
907+
stackTrace: s,
908+
);
909+
throw Exception("Invalid OP_RETURN data: $e");
910+
}
911+
}
855912
if (isMweb) {
856913
if (hasNonWitnessInput) {
857914
throw Exception("Found non witness input in mweb tx");

0 commit comments

Comments
 (0)