Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions lib/wallets/wallet/impl/ethereum_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -683,18 +683,41 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface {
Future<TxData> confirmSend({
required TxData txData,
TxData Function(TxData txData, String myAddress)? prepareTempTx,
Future<void> Function(String raw)? beforeBroadcast,
}) async {
final client = getEthClient();
if (_credentials == null) {
await _initCredentials();
}

try {
final txid = await client.sendTransaction(
_credentials!,
txData.web3dartTransaction!,
chainId: txData.chainId!.toInt(),
);
final String txid;
if (beforeBroadcast == null) {
txid = await client.sendTransaction(
_credentials!,
txData.web3dartTransaction!,
chainId: txData.chainId!.toInt(),
);
} else {
var signed = await client.signTransaction(
_credentials!,
txData.web3dartTransaction!,
chainId: txData.chainId!.toInt(),
);
if (txData.web3dartTransaction!.isEIP1559) {
signed = web3.prependTransactionType(0x02, signed);
}
txid = web3.bytesToHex(web3.keccak256(signed), include0x: true);
await beforeBroadcast(web3.bytesToHex(signed, include0x: true));
final response = await client
.sendRawTransaction(signed)
.timeout(const Duration(seconds: 30));
if (response.toLowerCase() != txid) {
throw StateError(
'Ethereum node returned an unexpected transaction ID.',
);
}
}

final data = (prepareTempTx ?? _prepareTempTx)(
txData.copyWith(txid: txid, txHash: txid),
Expand Down
19 changes: 8 additions & 11 deletions lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -272,17 +272,14 @@ class EthTokenWallet extends Wallet {
}

@override
Future<TxData> confirmSend({required TxData txData}) async {
try {
return await ethWallet.confirmSend(
txData: txData,
prepareTempTx: _prepareTempTx,
);
} catch (e) {
// rethrow to pass error in alert
rethrow;
}
}
Future<TxData> confirmSend({
required TxData txData,
Future<void> Function(String raw)? beforeBroadcast,
}) => ethWallet.confirmSend(
txData: txData,
prepareTempTx: _prepareTempTx,
beforeBroadcast: beforeBroadcast,
);

@override
Future<Amount> estimateFeeFor(Amount amount, BigInt feeRate) async {
Expand Down
90 changes: 31 additions & 59 deletions lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import '../impl/peercoin_wallet.dart';
import '../intermediate/bip39_hd_wallet.dart';
import 'cpfp_interface.dart';
import 'electrum_fee_planner.dart';
import 'firo_op_return.dart';
import 'mweb_fee_utils.dart';
import 'mweb_interface.dart';
import 'paynym_interface.dart';
Expand Down Expand Up @@ -563,7 +564,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>

final coinlib.CoinSelection selection = coinlib.CoinSelection.optimal(
candidates: candidates,
recipients: [recipientOutput],
recipients: [recipientOutput, ?_opReturnOutput(txData)],
changeProgram: changeProgram,
feePerKb: feePerKb,
minFee: minFee,
Expand Down Expand Up @@ -707,12 +708,22 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
}
}

coinlib.Output? _opReturnOutput(TxData txData) {
final hex = txData.opReturnData;
if (hex == null || hex.isEmpty) return null;
if (cryptoCurrency is! Firo) {
throw UnsupportedError('OP_RETURN sends are only supported for Firo');
}
return firoOpReturnOutput(hex);
}

/// Builds and signs a transaction
Future<TxData> buildTransaction({
required TxData txData,
required List<BaseInput> inputsWithKeys,
}) async {
Logging.instance.d("Starting buildTransaction ----------");
final opReturnOutput = _opReturnOutput(txData);

// temp tx data to show in gui while waiting for real data from server
final List<InputV2> tempInputs = [];
Expand Down Expand Up @@ -870,62 +881,16 @@ 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 (opReturnOutput != null) {
clTx = clTx.addOutput(opReturnOutput);
tempOutputs.add(
OutputV2.isarCantDoRequiredInDefaultConstructor(
scriptPubKeyHex: opReturnOutput.scriptPubKey.toHex,
valueStringSats: "0",
addresses: [],
walletOwns: false,
),
);
}
if (isMweb) {
if (hasNonWitnessInput) {
Expand Down Expand Up @@ -1863,13 +1828,20 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
}

@override
Future<TxData> confirmSend({required TxData txData}) async {
Future<TxData> confirmSend({
required TxData txData,
Future<void> Function(String raw)? beforeBroadcast,
}) async {
try {
Logging.instance.d("confirmSend txData: $txData");

final txHash = await electrumXClient.broadcastTransaction(
if (beforeBroadcast != null) await beforeBroadcast(txData.raw!);
final broadcasting = electrumXClient.broadcastTransaction(
rawTx: txData.raw!,
);
final txHash = await (beforeBroadcast == null
? broadcasting
: broadcasting.timeout(const Duration(seconds: 30)));
Logging.instance.d("Sent txHash: $txHash");

txData = txData.copyWith(
Expand Down
Loading
Loading