Skip to content

Commit 6a75856

Browse files
Merge pull request #1342 from navidR/release/navidr/add-op_return
Add OP_RETURN Support Required for Rosen Bridge
2 parents cfd97e4 + 1607850 commit 6a75856

8 files changed

Lines changed: 529 additions & 54 deletions

File tree

lib/pages/send_view/send_view.dart

Lines changed: 148 additions & 12 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+
_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+
"\u2014 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)

lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
1414
import '../../../models/paymint/fee_object_model.dart';
1515
import '../../../providers/providers.dart';
1616
import '../../../providers/ui/fee_rate_type_state_provider.dart';
17+
import '../../../providers/ui/preview_tx_button_state_provider.dart';
1718
import '../../../providers/wallet/public_private_balance_state_provider.dart';
1819
import '../../../themes/stack_colors.dart';
20+
import '../../../utilities/address_utils.dart';
1921
import '../../../utilities/amount/amount.dart';
2022
import '../../../utilities/amount/amount_formatter.dart';
2123
import '../../../utilities/constants.dart';
@@ -78,12 +80,54 @@ class _TransactionFeeSelectionSheetState
7880
"Calculating...",
7981
];
8082

83+
Amount _addFiroOpReturnFee({
84+
required Amount fee,
85+
required BigInt feeRate,
86+
required FiroWallet wallet,
87+
required CryptoCurrency coin,
88+
}) {
89+
final opReturnData = ref.read(pOpReturnData);
90+
if (opReturnData == null ||
91+
opReturnData.isEmpty ||
92+
ref.read(publicPrivateBalanceStateProvider) != BalanceType.public) {
93+
return fee;
94+
}
95+
96+
final extraOutputVSize = AddressUtils.opReturnOutputVSizeFromHex(
97+
opReturnData,
98+
);
99+
final extraFee = wallet.estimateTxFee(
100+
vSize: extraOutputVSize,
101+
feeRatePerKB: feeRate,
102+
);
103+
104+
return fee +
105+
Amount(
106+
rawValue: BigInt.from(extraFee),
107+
fractionDigits: coin.fractionDigits,
108+
);
109+
}
110+
81111
Future<Amount> feeFor({
82112
required Amount amount,
83113
required FeeRateType feeRateType,
84114
required BigInt feeRate,
85115
required CryptoCurrency coin,
86116
}) async {
117+
if (!widget.isToken &&
118+
coin is Firo &&
119+
ref.read(publicPrivateBalanceStateProvider) == BalanceType.public &&
120+
(ref.read(pOpReturnData)?.isNotEmpty ?? false)) {
121+
final wallet = ref.read(pWallets).getWallet(walletId) as FiroWallet;
122+
final fee = await wallet.estimateFeeFor(amount, feeRate);
123+
return _addFiroOpReturnFee(
124+
fee: fee,
125+
feeRate: feeRate,
126+
wallet: wallet,
127+
coin: coin,
128+
);
129+
}
130+
87131
switch (feeRateType) {
88132
case FeeRateType.fast:
89133
if (ref.read(feeSheetSessionCacheProvider).fast[amount] == null) {

0 commit comments

Comments
 (0)