Add OP_RETURN support required for rosen bridge
What changed, and why it matters
This commit adds support for embedding small pieces of public metadata (called OP_RETURN data) into cryptocurrency transactions, specifically to enable a feature called Rosen Bridge. The code lets users paste a special payment link or scan a QR code that includes bridge instructions, shows a warning if the user tries to use a private balance, and only allows the metadata on public Firo transactions. It also enforces an 80-byte size limit and validates the data before adding it to the transaction.
Review the OP_RETURN construction logic for non-Firo currencies and ensure the hex parser cannot be abused to create non-standard or oversized scripts. Verify that the UI cannot be bypassed to attach OP_RETURN data to private Spark transactions, and add tests covering malformed hex, oversized payloads, and boundary pushdata sizes (75/76/80 bytes).
Security signals we found
New OP_RETURN output construction in transaction building path
Input from payment URI/QR code flows into transaction script data
Explicit 80-byte OP_RETURN size cap enforced
Private-balance/Spark mode explicitly blocks OP_RETURN
Pushdata encoding uses OP_PUSHDATA1 for 76-80 byte payloads
No visible sanitization of arbitrary hex beyond length and parse checks
UI state reset on address clear / parse failure
Evidence from the diff
The patch introduces a new pOpReturnData provider and threads an optional opReturnData field through TxData and the send flows in both mobile and desktop views. AddressUtils.parsePaymentUri now recognizes the op_return URI parameter, and a new formatOpReturnTooltip helper decodes/display Rosen Bridge payloads. In ElectrumXInterface, OP_RETURN output construction is gated to Firo public transactions, enforces an 80-byte limit, and uses correct Bitcoin script pushdata encoding (direct push ≤75 bytes, OP_PUSHDATA1 for 76–80 bytes). UI guards disable OP_RETURN when the Spark (private) balance is selected and warn the user accordingly.
Changed components
lib/pages/send_view/send_view.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dartlib/providers/ui/preview_tx_button_state_provider.dartlib/utilities/address_utils.dartlib/wallets/models/tx_data.dartlib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dartInspect captured patch +328 / −43
diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart
index a2dd4f4..48f3290 100644
--- a/lib/pages/send_view/send_view.dart
+++ b/lib/pages/send_view/send_view.dart
@@ -160,7 +160,6 @@ class _SendViewState extends ConsumerState<SendView> {
try {
// auto fill address
_address = paymentData.address.trim();
- sendToController.text = _address!;
// autofill notes field
if (paymentData.message != null) {
@@ -180,7 +179,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;
});
@@ -923,6 +940,7 @@ class _SendViewState extends ConsumerState<SendView> {
selectedUTXOs.isNotEmpty)
? selectedUTXOs
: null,
+ opReturnData: ref.read(pOpReturnData),
),
);
} else if (wallet is FiroWallet) {
@@ -964,6 +982,7 @@ class _SendViewState extends ConsumerState<SendView> {
utxos: (coinControlEnabled && selectedUTXOs.isNotEmpty)
? selectedUTXOs
: null,
+ opReturnData: ref.read(pOpReturnData),
),
);
}
@@ -1136,6 +1155,7 @@ class _SendViewState extends ConsumerState<SendView> {
memoController.text = "";
_address = "";
_addressToggleFlag = false;
+ ref.read(pOpReturnData.notifier).state = null;
if (mounted) {
setState(() {});
}
@@ -1726,9 +1746,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,
@@ -1737,6 +1758,8 @@ class _SendViewState extends ConsumerState<SendView> {
if (parsed != null) {
_applyUri(parsed);
} else {
+ ref.read(pOpReturnData.notifier).state =
+ null;
await _checkSparkNameAndOrSetAddress(
newValue,
);
@@ -1949,6 +1972,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;
@@ -2666,16 +2721,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)
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
index efc02f6..72b646e 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
@@ -650,6 +650,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
ref.read(pDesktopUseUTXOs).isNotEmpty)
? ref.read(pDesktopUseUTXOs)
: null,
+ opReturnData: ref.read(pOpReturnData),
),
);
}
@@ -920,8 +921,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 ?? "";
@@ -1050,8 +1054,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);
}
@@ -1068,6 +1075,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 @
@@ -1754,14 +1762,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 {
@@ -1815,6 +1827,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
onTap: () {
sendToController.text = "";
_address = "";
+ ref.read(pOpReturnData.notifier).state =
+ null;
_setValidAddressProviders(_address);
setState(() {
_addressToggleFlag = false;
@@ -1960,6 +1974,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))
diff --git a/lib/providers/ui/preview_tx_button_state_provider.dart b/lib/providers/ui/preview_tx_button_state_provider.dart
index fcb77fe..1ad75aa 100644
--- a/lib/providers/ui/preview_tx_button_state_provider.dart
+++ b/lib/providers/ui/preview_tx_button_state_provider.dart
@@ -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,
@@ -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;
diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart
index ff0880c..43e72b6 100644
--- a/lib/utilities/address_utils.dart
+++ b/lib/utilities/address_utils.dart
@@ -23,6 +23,7 @@ class AddressUtils {
'tx_payment_id',
'recipient_name',
'tx_description',
+ 'op_return', // For Rosen Bridge and other OP_RETURN protocols.
// TODO [prio=med]: Add more recognized params for other coins.
};
@@ -268,24 +269,85 @@ class AddressUtils {
if ((mimblewimblecoinAddress.startsWith("http://") ||
mimblewimblecoinAddress.startsWith("https://")) &&
mimblewimblecoinAddress.contains("@")) {
- mimblewimblecoinAddress =
- mimblewimblecoinAddress.replaceAll("http://", "");
- mimblewimblecoinAddress =
- mimblewimblecoinAddress.replaceAll("https://", "");
+ mimblewimblecoinAddress = mimblewimblecoinAddress.replaceAll(
+ "http://",
+ "",
+ );
+ mimblewimblecoinAddress = mimblewimblecoinAddress.replaceAll(
+ "https://",
+ "",
+ );
}
// strip mailto: prefix
if (mimblewimblecoinAddress.startsWith("mailto:")) {
- mimblewimblecoinAddress =
- mimblewimblecoinAddress.replaceAll("mailto:", "");
+ mimblewimblecoinAddress = mimblewimblecoinAddress.replaceAll(
+ "mailto:",
+ "",
+ );
}
// strip / suffix if the address contains an @ symbol (and is thus an mwcmqs address)
if (mimblewimblecoinAddress.endsWith("/") &&
mimblewimblecoinAddress.contains("@")) {
mimblewimblecoinAddress = mimblewimblecoinAddress.substring(
- 0, mimblewimblecoinAddress.length - 1);
+ 0,
+ mimblewimblecoinAddress.length - 1,
+ );
}
return mimblewimblecoinAddress;
}
+
+ /// Formats OP_RETURN hex data for display in tooltip.
+ /// If data matches Rosen Bridge format, shows structured fields.
+ /// Otherwise returns the raw hex with a generic description.
+ static String formatOpReturnTooltip(String hex) {
+ // Rosen Bridge OP_RETURN format:
+ // toChain(1B) + bridgeFee(8B) + networkFee(8B) + addrLen(1B) + toAddress(var)
+ const minRosenLen = 36; // minimum 18 bytes
+ if (hex.length < minRosenLen) {
+ return "Raw OP_RETURN data:\n$hex";
+ }
+
+ try {
+ const chains = [
+ 'ergo',
+ 'cardano',
+ 'bitcoin',
+ 'ethereum',
+ 'binance',
+ 'doge',
+ 'bitcoin-runes',
+ 'firo',
+ ];
+
+ final toChainCode = int.parse(hex.substring(0, 2), radix: 16);
+ if (toChainCode >= chains.length) {
+ return "Raw OP_RETURN data:\n$hex";
+ }
+
+ final bridgeFee = BigInt.parse(
+ hex.substring(2, 18),
+ radix: 16,
+ ).toString();
+ final networkFee = BigInt.parse(
+ hex.substring(18, 34),
+ radix: 16,
+ ).toString();
+ final addrLen = int.parse(hex.substring(34, 36), radix: 16);
+ final addrEnd = 36 + addrLen * 2;
+ if (hex.length < addrEnd) {
+ return "Raw OP_RETURN data:\n$hex";
+ }
+ final toAddressHex = hex.substring(36, addrEnd);
+
+ return "Rosen Bridge data\n"
+ " To chain: ${chains[toChainCode]}\n"
+ " Bridge fee: $bridgeFee\n"
+ " Network fee: $networkFee\n"
+ " To address (hex): $toAddressHex";
+ } catch (_) {
+ return "Raw OP_RETURN data:\n$hex";
+ }
+ }
}
class PaymentUriData {
diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart
index 14c7186..bdff31c 100644
--- a/lib/wallets/models/tx_data.dart
+++ b/lib/wallets/models/tx_data.dart
@@ -112,6 +112,9 @@ class TxData {
final bool salviumStakeTx;
+ // Generic OP_RETURN data (hex string) - for Rosen Bridge and other protocols
+ final String? opReturnData;
+
TxData({
this.feeRateType,
this.feeRateAmount,
@@ -149,6 +152,7 @@ class TxData {
this.sparkNameInfo,
this.vExtraData,
this.overrideVersion,
+ this.opReturnData,
this.type = TxType.regular,
this.salviumStakeTx = false,
});
@@ -263,6 +267,7 @@ class TxData {
String? noteOnChain,
String? memo,
String? otherData,
+ String? opReturnData,
Set<BaseInput>? utxos,
List<BaseInput>? usedUTXOs,
List<TxRecipient>? recipients,
@@ -341,6 +346,7 @@ class TxData {
sparkNameInfo: sparkNameInfo ?? this.sparkNameInfo,
vExtraData: vExtraData ?? this.vExtraData,
overrideVersion: overrideVersion ?? this.overrideVersion,
+ opReturnData: opReturnData ?? this.opReturnData,
type: type ?? this.type,
);
}
@@ -383,6 +389,7 @@ class TxData {
'sparkNameInfo: $sparkNameInfo, '
'vExtraData: ${vExtraData?.toHex}, '
'overrideVersion: $overrideVersion, '
+ 'opReturnData: $opReturnData, '
'type: $type, '
'}';
}
diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart
index e963566..91496e7 100644
--- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart
+++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart
@@ -852,6 +852,63 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
);
}
+ // Add OP_RETURN output if provided (for Rosen Bridge and other protocols)
+ // Currently only supported for Firo
+ if (cryptoCurrency is Firo &&
+ txData.opReturnData != null &&
+ txData.opReturnData!.isNotEmpty) {
+ try {
+ final opReturnBytes = txData.opReturnData!.toUint8ListFromHex;
+
+ // Validate OP_RETURN size (Bitcoin/Firo limit is 80 bytes)
+ if (opReturnBytes.length > 80) {
+ throw Exception(
+ "OP_RETURN data exceeds 80 byte limit: ${opReturnBytes.length} bytes",
+ );
+ }
+
+ // Encode push data: OP_PUSHDATA1 (0x4c) for 76-80 bytes, direct length otherwise
+ final pushData = opReturnBytes.length <= 75
+ ? Uint8List.fromList([opReturnBytes.length, ...opReturnBytes])
+ : Uint8List.fromList([
+ 0x4c,
+ opReturnBytes.length,
+ ...opReturnBytes,
+ ]);
+
+ final opReturnScript = Uint8List.fromList([
+ 0x6a, // OP_RETURN opcode
+ ...pushData,
+ ]);
+
+ final opReturnOutput = coinlib.Output.fromScriptBytes(
+ BigInt.zero, // OP_RETURN outputs have 0 value
+ opReturnScript,
+ );
+
+ clTx = clTx.addOutput(opReturnOutput);
+
+ Logging.instance.i(
+ "Added OP_RETURN output with ${opReturnBytes.length} bytes of data",
+ );
+
+ tempOutputs.add(
+ OutputV2.isarCantDoRequiredInDefaultConstructor(
+ scriptPubKeyHex: opReturnScript.toHex,
+ valueStringSats: "0",
+ addresses: [],
+ walletOwns: false,
+ ),
+ );
+ } catch (e, s) {
+ Logging.instance.e(
+ "Failed to add OP_RETURN output",
+ error: e,
+ stackTrace: s,
+ );
+ throw Exception("Invalid OP_RETURN data: $e");
+ }
+ }
if (isMweb) {
if (hasNonWitnessInput) {
throw Exception("Found non witness input in mweb tx");
Why this scored 37/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.