non lelantus firo sends

This commit is contained in:
julian 2022-09-07 09:56:10 -06:00
parent f8c8dcabfc
commit 15c4d0e617
4 changed files with 768 additions and 46 deletions

View file

@ -9,6 +9,7 @@ import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/utilities/cfcolors.dart'; import 'package:stackwallet/utilities/cfcolors.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
@ -19,6 +20,8 @@ import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_dialog.dart';
import '../../providers/wallet/public_private_balance_state_provider.dart';
class ConfirmTransactionView extends ConsumerStatefulWidget { class ConfirmTransactionView extends ConsumerStatefulWidget {
const ConfirmTransactionView({ const ConfirmTransactionView({
Key? key, Key? key,
@ -56,10 +59,20 @@ class _ConfirmTransactionViewState
final note = transactionInfo["note"] as String? ?? ""; final note = transactionInfo["note"] as String? ?? "";
final manager = final manager =
ref.read(walletsChangeNotifierProvider).getManager(walletId); ref.read(walletsChangeNotifierProvider).getManager(walletId);
try { try {
final txid = await manager.confirmSend(txData: transactionInfo); String txid;
final coin = manager.coin;
if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
ref.read(publicPrivateBalanceStateProvider.state).state !=
"Private") {
txid = await (manager.wallet as FiroWallet)
.confirmSendPublic(txData: transactionInfo);
} else {
txid = await manager.confirmSend(txData: transactionInfo);
}
unawaited(manager.refresh()); unawaited(manager.refresh());
// save note // save note
@ -79,7 +92,7 @@ class _ConfirmTransactionViewState
showFloatingFlushBar( showFloatingFlushBar(
type: FlushBarType.warning, type: FlushBarType.warning,
message: message:
"Connection failed. Please check the address and try again.", "Connection failed. Please check the address and try again.",
context: context, context: context,
), ),
); );
@ -100,10 +113,10 @@ class _ConfirmTransactionViewState
message: e.toString(), message: e.toString(),
rightButton: TextButton( rightButton: TextButton(
style: Theme.of(context).textButtonTheme.style?.copyWith( style: Theme.of(context).textButtonTheme.style?.copyWith(
backgroundColor: MaterialStateProperty.all<Color>( backgroundColor: MaterialStateProperty.all<Color>(
CFColors.buttonGray, CFColors.buttonGray,
), ),
), ),
child: Text( child: Text(
"Ok", "Ok",
style: STextStyles.button.copyWith( style: STextStyles.button.copyWith(
@ -212,9 +225,9 @@ class _ConfirmTransactionViewState
.select((value) => value.locale), .select((value) => value.locale),
), ),
)} ${ref.watch( )} ${ref.watch(
managerProvider managerProvider
.select((value) => value.coin), .select((value) => value.coin),
).ticker}", ).ticker}",
style: STextStyles.itemSubtitle12, style: STextStyles.itemSubtitle12,
textAlign: TextAlign.right, textAlign: TextAlign.right,
), ),
@ -240,9 +253,9 @@ class _ConfirmTransactionViewState
.select((value) => value.locale), .select((value) => value.locale),
), ),
)} ${ref.watch( )} ${ref.watch(
managerProvider managerProvider
.select((value) => value.coin), .select((value) => value.coin),
).ticker}", ).ticker}",
style: STextStyles.itemSubtitle12, style: STextStyles.itemSubtitle12,
textAlign: TextAlign.right, textAlign: TextAlign.right,
), ),
@ -292,9 +305,9 @@ class _ConfirmTransactionViewState
.select((value) => value.locale), .select((value) => value.locale),
), ),
)} ${ref.watch( )} ${ref.watch(
managerProvider managerProvider
.select((value) => value.coin), .select((value) => value.coin),
).ticker}", ).ticker}",
style: STextStyles.itemSubtitle12, style: STextStyles.itemSubtitle12,
textAlign: TextAlign.right, textAlign: TextAlign.right,
), ),
@ -306,18 +319,18 @@ class _ConfirmTransactionViewState
), ),
TextButton( TextButton(
style: style:
Theme.of(context).textButtonTheme.style?.copyWith( Theme.of(context).textButtonTheme.style?.copyWith(
backgroundColor: backgroundColor:
MaterialStateProperty.all<Color>( MaterialStateProperty.all<Color>(
CFColors.stackAccent, CFColors.stackAccent,
), ),
), ),
onPressed: () async { onPressed: () async {
final unlocked = await Navigator.push( final unlocked = await Navigator.push(
context, context,
RouteGenerator.getRoute( RouteGenerator.getRoute(
shouldUseMaterialRoute: shouldUseMaterialRoute:
RouteGenerator.useMaterialPageRoute, RouteGenerator.useMaterialPageRoute,
builder: (_) => const LockscreenView( builder: (_) => const LockscreenView(
showBackButton: true, showBackButton: true,
popOnSuccess: true, popOnSuccess: true,
@ -325,9 +338,9 @@ class _ConfirmTransactionViewState
routeOnSuccess: "", routeOnSuccess: "",
biometricsCancelButtonString: "CANCEL", biometricsCancelButtonString: "CANCEL",
biometricsLocalizedReason: biometricsLocalizedReason:
"Authenticate to send transaction", "Authenticate to send transaction",
biometricsAuthenticationTitle: biometricsAuthenticationTitle:
"Confirm Transaction", "Confirm Transaction",
), ),
settings: const RouteSettings( settings: const RouteSettings(
name: "/confirmsendlockscreen"), name: "/confirmsendlockscreen"),

View file

@ -1333,9 +1333,28 @@ class _SendViewState extends ConsumerState<SendView> {
final amount = Format.decimalAmountToSatoshis( final amount = Format.decimalAmountToSatoshis(
_amountToSend!); _amountToSend!);
final availableBalance = int availableBalance;
Format.decimalAmountToSatoshis( if ((coin == Coin.firo ||
await manager.availableBalance); coin == Coin.firoTestNet)
) {
if (ref
.read(
publicPrivateBalanceStateProvider
.state)
.state ==
"Private") {
availableBalance = Format.decimalAmountToSatoshis(
await (manager.wallet as FiroWallet).availablePrivateBalance());
} else {
availableBalance = Format.decimalAmountToSatoshis(
await (manager.wallet as FiroWallet).availablePublicBalance());
}
} else {
availableBalance =
Format.decimalAmountToSatoshis(
await manager.availableBalance);
}
// confirm send all // confirm send all
if (amount == availableBalance) { if (amount == availableBalance) {
@ -1419,14 +1438,36 @@ class _SendViewState extends ConsumerState<SendView> {
}, },
)); ));
final txData = await manager.prepareSend( Map<String, dynamic> txData;
address: _address!,
satoshiAmount: amount, if ((coin == Coin.firo ||
args: { coin == Coin.firoTestNet) &&
"feeRate": ref
ref.read(feeRateTypeStateProvider) .read(
}, publicPrivateBalanceStateProvider
); .state)
.state !=
"Private") {
txData =
await (manager.wallet as FiroWallet)
.prepareSendPublic(
address: _address!,
satoshiAmount: amount,
args: {
"feeRate":
ref.read(feeRateTypeStateProvider)
},
);
} else {
txData = await manager.prepareSend(
address: _address!,
satoshiAmount: amount,
args: {
"feeRate":
ref.read(feeRateTypeStateProvider)
},
);
}
if (!wasCancelled && mounted) { if (!wasCancelled && mounted) {
// pop building dialog // pop building dialog

View file

@ -10,7 +10,7 @@ class TxIcon extends StatelessWidget {
static const Size size = Size(32, 32); static const Size size = Size(32, 32);
String _getAssetName(bool isCancelled, bool isReceived, bool isPending) { String _getAssetName(bool isCancelled, bool isReceived, bool isPending) {
if (transaction.subType == "mint") { if (!isReceived && transaction.subType == "mint") {
if (isCancelled) { if (isCancelled) {
return Assets.svg.anonymizeFailed; return Assets.svg.anonymizeFailed;
} }

View file

@ -37,12 +37,14 @@ import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/prefs.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
const DUST_LIMIT = 1000;
const MINIMUM_CONFIRMATIONS = 1; const MINIMUM_CONFIRMATIONS = 1;
const MINT_LIMIT = 100100000000; const MINT_LIMIT = 100100000000;
const int LELANTUS_VALUE_SPEND_LIMIT_PER_TRANSACTION = 5001 * 100000000; const int LELANTUS_VALUE_SPEND_LIMIT_PER_TRANSACTION = 5001 * 100000000;
@ -973,11 +975,117 @@ class FiroWallet extends CoinServiceAPI {
@override @override
bool get isConnected => _isConnected; bool get isConnected => _isConnected;
Future<Map<String, dynamic>> prepareSendPublic({
required String address,
required int satoshiAmount,
Map<String, dynamic>? args,
}) async {
try {
final feeRateType = args?["feeRate"];
final feeRateAmount = args?["feeRateAmount"];
if (feeRateType is FeeRateType || feeRateAmount is int) {
late final int rate;
if (feeRateType is FeeRateType) {
int fee = 0;
final feeObject = await fees;
switch (feeRateType) {
case FeeRateType.fast:
fee = feeObject.fast;
break;
case FeeRateType.average:
fee = feeObject.medium;
break;
case FeeRateType.slow:
fee = feeObject.slow;
break;
}
rate = fee;
} else {
rate = feeRateAmount as int;
}
// check for send all
bool isSendAll = false;
final balance =
Format.decimalAmountToSatoshis(await availablePublicBalance());
if (satoshiAmount == balance) {
isSendAll = true;
}
final txData =
await coinSelection(satoshiAmount, rate, address, isSendAll);
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
try {
if (txData is int) {
switch (txData) {
case 1:
throw Exception("Insufficient balance!");
case 2:
throw Exception(
"Insufficient funds to pay for transaction fee!");
default:
throw Exception("Transaction failed with error code $txData");
}
} else {
final hex = txData["hex"];
if (hex is String) {
final fee = txData["fee"] as int;
final vSize = txData["vSize"] as int;
Logging.instance
.log("prepared txHex: $hex", level: LogLevel.Info);
Logging.instance.log("prepared fee: $fee", level: LogLevel.Info);
Logging.instance
.log("prepared vSize: $vSize", level: LogLevel.Info);
// fee should never be less than vSize sanity check
if (fee < vSize) {
throw Exception(
"Error in fee calculation: Transaction fee cannot be less than vSize");
}
return txData as Map<String, dynamic>;
} else {
throw Exception("prepared hex is not a String!!!");
}
}
} catch (e, s) {
Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s",
level: LogLevel.Error);
rethrow;
}
} else {
throw ArgumentError("Invalid fee rate argument provided!");
}
} catch (e, s) {
Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s",
level: LogLevel.Error);
rethrow;
}
}
Future<String> confirmSendPublic({dynamic txData}) async {
try {
Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info);
final txHash = await _electrumXClient.broadcastTransaction(
rawTx: txData["hex"] as String);
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
return txHash;
} catch (e, s) {
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
level: LogLevel.Error);
rethrow;
}
}
@override @override
Future<Map<String, dynamic>> prepareSend( Future<Map<String, dynamic>> prepareSend({
{required String address, required String address,
required int satoshiAmount, required int satoshiAmount,
Map<String, dynamic>? args}) async { Map<String, dynamic>? args,
}) async {
try { try {
dynamic txHexOrError = dynamic txHexOrError =
await _createJoinSplitTransaction(satoshiAmount, address, false); await _createJoinSplitTransaction(satoshiAmount, address, false);
@ -1129,6 +1237,531 @@ class FiroWallet extends CoinServiceAPI {
isolates.clear(); isolates.clear();
} }
int estimateTxFee({required int vSize, required int feeRatePerKB}) {
return vSize * (feeRatePerKB / 1000).ceil();
}
/// The coinselection algorithm decides whether or not the user is eligible to make the transaction
/// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return
/// a map containing the tx hex along with other important information. If not, then it will return
/// an integer (1 or 2)
dynamic coinSelection(
int satoshiAmountToSend,
int selectedTxFeeRate,
String _recipientAddress,
bool isSendAll, {
int additionalOutputs = 0,
List<UtxoObject>? utxos,
}) async {
Logging.instance
.log("Starting coinSelection ----------", level: LogLevel.Info);
final List<UtxoObject> availableOutputs = utxos ?? _outputsList;
final List<UtxoObject> spendableOutputs = [];
int spendableSatoshiValue = 0;
// Build list of spendable outputs and totaling their satoshi amount
for (var i = 0; i < availableOutputs.length; i++) {
if (availableOutputs[i].blocked == false &&
availableOutputs[i].status.confirmed == true) {
spendableOutputs.add(availableOutputs[i]);
spendableSatoshiValue += availableOutputs[i].value;
}
}
// sort spendable by age (oldest first)
spendableOutputs.sort(
(a, b) => b.status.confirmations.compareTo(a.status.confirmations));
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
level: LogLevel.Info);
Logging.instance
.log("spendableOutputs: $spendableOutputs", level: LogLevel.Info);
Logging.instance.log("spendableSatoshiValue: $spendableSatoshiValue",
level: LogLevel.Info);
Logging.instance
.log("satoshiAmountToSend: $satoshiAmountToSend", level: LogLevel.Info);
// If the amount the user is trying to send is smaller than the amount that they have spendable,
// then return 1, which indicates that they have an insufficient balance.
if (spendableSatoshiValue < satoshiAmountToSend) {
return 1;
// If the amount the user wants to send is exactly equal to the amount they can spend, then return
// 2, which indicates that they are not leaving enough over to pay the transaction fee
} else if (spendableSatoshiValue == satoshiAmountToSend && !isSendAll) {
return 2;
}
// If neither of these statements pass, we assume that the user has a spendable balance greater
// than the amount they're attempting to send. Note that this value still does not account for
// the added transaction fee, which may require an extra input and will need to be checked for
// later on.
// Possible situation right here
int satoshisBeingUsed = 0;
int inputsBeingConsumed = 0;
List<UtxoObject> utxoObjectsToUse = [];
for (var i = 0;
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[i]);
satoshisBeingUsed += spendableOutputs[i].value;
inputsBeingConsumed += 1;
}
for (int i = 0;
i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length;
i++) {
utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]);
satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value;
inputsBeingConsumed += 1;
}
Logging.instance
.log("satoshisBeingUsed: $satoshisBeingUsed", level: LogLevel.Info);
Logging.instance
.log("inputsBeingConsumed: $inputsBeingConsumed", level: LogLevel.Info);
Logging.instance
.log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info);
// numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray
List<String> recipientsArray = [_recipientAddress];
List<int> recipientsAmtArray = [satoshiAmountToSend];
// gather required signing data
final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse);
if (isSendAll) {
Logging.instance
.log("Attempting to send all $coin", level: LogLevel.Info);
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
int feeForOneOutput = estimateTxFee(
vSize: vSizeForOneOutput,
feeRatePerKB: selectedTxFeeRate,
);
if (feeForOneOutput < vSizeForOneOutput + 1) {
feeForOneOutput = vSizeForOneOutput + 1;
}
final int amount = satoshiAmountToSend - feeForOneOutput;
dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: recipientsArray,
satoshiAmounts: [amount],
);
Map<String, dynamic> transactionObject = {
"hex": txn["hex"],
"recipient": recipientsArray[0],
"recipientAmt": amount,
"fee": feeForOneOutput,
"vSize": txn["vSize"],
};
return transactionObject;
}
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
await _getCurrentAddressForChain(1),
],
satoshiAmounts: [
satoshiAmountToSend,
satoshisBeingUsed - satoshiAmountToSend - 1,
], // dust limit is the minimum amount a change output should be
))["vSize"] as int;
debugPrint("vSizeForOneOutput $vSizeForOneOutput");
debugPrint("vSizeForTwoOutPuts $vSizeForTwoOutPuts");
// Assume 1 output, only for recipient and no change
var feeForOneOutput = estimateTxFee(
vSize: vSizeForOneOutput,
feeRatePerKB: selectedTxFeeRate,
);
// Assume 2 outputs, one for recipient and one for change
var feeForTwoOutputs = estimateTxFee(
vSize: vSizeForTwoOutPuts,
feeRatePerKB: selectedTxFeeRate,
);
Logging.instance
.log("feeForTwoOutputs: $feeForTwoOutputs", level: LogLevel.Info);
Logging.instance
.log("feeForOneOutput: $feeForOneOutput", level: LogLevel.Info);
if (feeForOneOutput < (vSizeForOneOutput + 1)) {
feeForOneOutput = (vSizeForOneOutput + 1);
}
if (feeForTwoOutputs < ((vSizeForTwoOutPuts + 1))) {
feeForTwoOutputs = ((vSizeForTwoOutPuts + 1));
}
Logging.instance
.log("feeForTwoOutputs: $feeForTwoOutputs", level: LogLevel.Info);
Logging.instance
.log("feeForOneOutput: $feeForOneOutput", level: LogLevel.Info);
if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) {
if (satoshisBeingUsed - satoshiAmountToSend >
feeForOneOutput + DUST_LIMIT) {
// Here, we know that theoretically, we may be able to include another output(change) but we first need to
// factor in the value of this output in satoshis.
int changeOutputSize =
satoshisBeingUsed - satoshiAmountToSend - feeForTwoOutputs;
// We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and
// the second output's size > DUST_LIMIT satoshis, we perform the mechanics required to properly generate and use a new
// change address.
if (changeOutputSize > DUST_LIMIT &&
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize ==
feeForTwoOutputs) {
// generate new change address if current change address has been used
await checkChangeAddressForTransactions();
final String newChangeAddress = await _getCurrentAddressForChain(1);
int feeBeingPaid =
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
recipientsArray.add(newChangeAddress);
recipientsAmtArray.add(changeOutputSize);
// At this point, we have the outputs we're going to use, the amounts to send along with which addresses
// we intend to send these amounts to. We have enough to send instructions to build the transaction.
Logging.instance.log('2 outputs in tx', level: LogLevel.Info);
Logging.instance
.log('Input size: $satoshisBeingUsed', level: LogLevel.Info);
Logging.instance.log('Recipient output size: $satoshiAmountToSend',
level: LogLevel.Info);
Logging.instance.log('Change Output Size: $changeOutputSize',
level: LogLevel.Info);
Logging.instance.log(
'Difference (fee being paid): $feeBeingPaid sats',
level: LogLevel.Info);
Logging.instance
.log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info);
dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray,
);
// make sure minimum fee is accurate if that is being used
if (txn["vSize"] - feeBeingPaid == 1) {
int changeOutputSize =
satoshisBeingUsed - satoshiAmountToSend - (txn["vSize"] as int);
feeBeingPaid =
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
recipientsAmtArray.removeLast();
recipientsAmtArray.add(changeOutputSize);
Logging.instance.log('Adjusted Input size: $satoshisBeingUsed',
level: LogLevel.Info);
Logging.instance.log(
'Adjusted Recipient output size: $satoshiAmountToSend',
level: LogLevel.Info);
Logging.instance.log(
'Adjusted Change Output Size: $changeOutputSize',
level: LogLevel.Info);
Logging.instance.log(
'Adjusted Difference (fee being paid): $feeBeingPaid sats',
level: LogLevel.Info);
Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs',
level: LogLevel.Info);
txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray,
);
}
Map<String, dynamic> transactionObject = {
"hex": txn["hex"],
"recipient": recipientsArray[0],
"recipientAmt": recipientsAmtArray[0],
"fee": feeBeingPaid,
"vSize": txn["vSize"],
};
return transactionObject;
} else {
// Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize
// is smaller than or equal to [DUST_LIMIT]. Revert to single output transaction.
Logging.instance.log('1 output in tx', level: LogLevel.Info);
Logging.instance
.log('Input size: $satoshisBeingUsed', level: LogLevel.Info);
Logging.instance.log('Recipient output size: $satoshiAmountToSend',
level: LogLevel.Info);
Logging.instance.log(
'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats',
level: LogLevel.Info);
Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray,
);
Map<String, dynamic> transactionObject = {
"hex": txn["hex"],
"recipient": recipientsArray[0],
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
};
return transactionObject;
}
} else {
// No additional outputs needed since adding one would mean that it'd be smaller than 546 sats
// which makes it uneconomical to add to the transaction. Here, we pass data directly to instruct
// the wallet to begin crafting the transaction that the user requested.
Logging.instance.log('1 output in tx', level: LogLevel.Info);
Logging.instance
.log('Input size: $satoshisBeingUsed', level: LogLevel.Info);
Logging.instance.log('Recipient output size: $satoshiAmountToSend',
level: LogLevel.Info);
Logging.instance.log(
'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats',
level: LogLevel.Info);
Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray,
);
Map<String, dynamic> transactionObject = {
"hex": txn["hex"],
"recipient": recipientsArray[0],
"recipientAmt": recipientsAmtArray[0],
"fee": satoshisBeingUsed - satoshiAmountToSend,
"vSize": txn["vSize"],
};
return transactionObject;
}
} else if (satoshisBeingUsed - satoshiAmountToSend == feeForOneOutput) {
// In this scenario, no additional change output is needed since inputs - outputs equal exactly
// what we need to pay for fees. Here, we pass data directly to instruct the wallet to begin
// crafting the transaction that the user requested.
Logging.instance.log('1 output in tx', level: LogLevel.Info);
Logging.instance
.log('Input size: $satoshisBeingUsed', level: LogLevel.Info);
Logging.instance.log('Recipient output size: $satoshiAmountToSend',
level: LogLevel.Info);
Logging.instance.log(
'Fee being paid: ${satoshisBeingUsed - satoshiAmountToSend} sats',
level: LogLevel.Info);
Logging.instance
.log('Estimated fee: $feeForOneOutput', level: LogLevel.Info);
dynamic txn = await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: recipientsArray,
satoshiAmounts: recipientsAmtArray,
);
Map<String, dynamic> transactionObject = {
"hex": txn["hex"],
"recipient": recipientsArray[0],
"recipientAmt": recipientsAmtArray[0],
"fee": feeForOneOutput,
"vSize": txn["vSize"],
};
return transactionObject;
} else {
// Remember that returning 2 indicates that the user does not have a sufficient balance to
// pay for the transaction fee. Ideally, at this stage, we should check if the user has any
// additional outputs they're able to spend and then recalculate fees.
Logging.instance.log(
'Cannot pay tx fee - checking for more outputs and trying again',
level: LogLevel.Warning);
// try adding more outputs
if (spendableOutputs.length > inputsBeingConsumed) {
return coinSelection(satoshiAmountToSend, selectedTxFeeRate,
_recipientAddress, isSendAll,
additionalOutputs: additionalOutputs + 1, utxos: utxos);
}
return 2;
}
}
Future<Map<String, dynamic>> fetchBuildTxData(
List<UtxoObject> utxosToUse,
) async {
// return data
Map<String, dynamic> results = {};
Map<String, List<String>> addressTxid = {};
// addresses to check
List<String> addresses = [];
try {
// Populating the addresses to check
for (var i = 0; i < utxosToUse.length; i++) {
final txid = utxosToUse[i].txid;
final tx = await _cachedElectrumXClient.getTransaction(
txHash: txid,
coin: coin,
);
for (final output in tx["vout"] as List) {
final n = output["n"];
if (n != null && n == utxosToUse[i].vout) {
final address = output["scriptPubKey"]["addresses"][0] as String;
if (!addressTxid.containsKey(address)) {
addressTxid[address] = <String>[];
}
(addressTxid[address] as List).add(txid);
addresses.add(address);
}
}
}
// p2pkh / bip44
final addressesLength = addresses.length;
if (addressesLength > 0) {
final receiveDerivationsString =
await _secureStore.read(key: "${walletId}_receiveDerivations");
final receiveDerivations = Map<String, dynamic>.from(
jsonDecode(receiveDerivationsString ?? "{}") as Map);
final changeDerivationsString =
await _secureStore.read(key: "${walletId}_changeDerivations");
final changeDerivations = Map<String, dynamic>.from(
jsonDecode(changeDerivationsString ?? "{}") as Map);
for (int i = 0; i < addressesLength; i++) {
// receives
dynamic receiveDerivation;
for (int j = 0; j < receiveDerivations.length; j++) {
if (receiveDerivations["$j"]["address"] == addresses[i]) {
receiveDerivation = receiveDerivations["$j"];
}
}
// receiveDerivation = receiveDerivations[addresses[i]];
// if a match exists it will not be null
if (receiveDerivation != null) {
final data = P2PKH(
data: PaymentData(
pubkey: Format.stringToUint8List(
receiveDerivation["publicKey"] as String)),
network: _network,
).data;
for (String tx in addressTxid[addresses[i]]!) {
results[tx] = {
"output": data.output,
"keyPair": ECPair.fromWIF(
receiveDerivation["wif"] as String,
network: _network,
),
};
}
} else {
// if its not a receive, check change
dynamic changeDerivation;
for (int j = 0; j < changeDerivations.length; j++) {
if (changeDerivations["$j"]["address"] == addresses[i]) {
changeDerivation = changeDerivations["$j"];
}
}
// final changeDerivation = changeDerivations[addresses[i]];
// if a match exists it will not be null
if (changeDerivation != null) {
final data = P2PKH(
data: PaymentData(
pubkey: Format.stringToUint8List(
changeDerivation["publicKey"] as String)),
network: _network,
).data;
for (String tx in addressTxid[addresses[i]]!) {
results[tx] = {
"output": data.output,
"keyPair": ECPair.fromWIF(
changeDerivation["wif"] as String,
network: _network,
),
};
}
}
}
}
}
return results;
} catch (e, s) {
Logging.instance
.log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error);
rethrow;
}
}
/// Builds and signs a transaction
Future<Map<String, dynamic>> buildTransaction({
required List<UtxoObject> utxosToUse,
required Map<String, dynamic> utxoSigningData,
required List<String> recipients,
required List<int> satoshiAmounts,
}) async {
Logging.instance
.log("Starting buildTransaction ----------", level: LogLevel.Info);
final txb = TransactionBuilder(network: _network);
txb.setVersion(1);
// Add transaction inputs
for (var i = 0; i < utxosToUse.length; i++) {
final txid = utxosToUse[i].txid;
txb.addInput(txid, utxosToUse[i].vout, null,
utxoSigningData[txid]["output"] as Uint8List);
}
// Add transaction output
for (var i = 0; i < recipients.length; i++) {
txb.addOutput(recipients[i], satoshiAmounts[i]);
}
try {
// Sign the transaction accordingly
for (var i = 0; i < utxosToUse.length; i++) {
final txid = utxosToUse[i].txid;
txb.sign(
vin: i,
keyPair: utxoSigningData[txid]["keyPair"] as ECPair,
witnessValue: utxosToUse[i].value,
redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?,
);
}
} catch (e, s) {
Logging.instance.log("Caught exception while signing transaction: $e\n$s",
level: LogLevel.Error);
rethrow;
}
final builtTx = txb.build();
final vSize = builtTx.virtualSize();
return {"hex": builtTx.toHex(), "vSize": vSize};
}
@override @override
Future<void> updateNode(bool shouldRefresh) async { Future<void> updateNode(bool shouldRefresh) async {
final failovers = NodeService() final failovers = NodeService()
@ -2140,9 +2773,11 @@ class FiroWallet extends CoinServiceAPI {
!listLelantusTxData.containsKey(value.txid)) { !listLelantusTxData.containsKey(value.txid)) {
// Every receive should be listed whether minted or not. // Every receive should be listed whether minted or not.
listLelantusTxData[value.txid] = value; listLelantusTxData[value.txid] = value;
} else if (value.txType == "Sent" && } else if (value.txType == "Sent"
hasAtLeastOneReceive && // &&
value.subType == "mint") { // hasAtLeastOneReceive &&
// value.subType == "mint"
) {
listLelantusTxData[value.txid] = value; listLelantusTxData[value.txid] = value;
// use mint sends to update receives with user readable values. // use mint sends to update receives with user readable values.
@ -2402,12 +3037,45 @@ class FiroWallet extends CoinServiceAPI {
} }
} on SocketException catch (se, s) { } on SocketException catch (se, s) {
Logging.instance.log( Logging.instance.log(
"SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s", "SocketException caught in checkReceivingAddressForTransactions(): $se\n$s",
level: LogLevel.Error); level: LogLevel.Error);
return; return;
} catch (e, s) { } catch (e, s) {
Logging.instance.log( Logging.instance.log(
"Exception rethrown from _checkReceivingAddressForTransactions(): $e\n$s", "Exception rethrown from checkReceivingAddressForTransactions(): $e\n$s",
level: LogLevel.Error);
rethrow;
}
}
Future<void> checkChangeAddressForTransactions() async {
try {
final String currentExternalAddr = await _getCurrentAddressForChain(1);
final int numtxs =
await _getReceivedTxCount(address: currentExternalAddr);
Logging.instance.log(
'Number of txs for current change address: $currentExternalAddr: $numtxs',
level: LogLevel.Info);
if (numtxs >= 1) {
await incrementAddressIndexForChain(
0); // First increment the change index
final newReceivingIndex =
DB.instance.get<dynamic>(boxName: walletId, key: 'changeIndex')
as int; // Check the new change index
final newReceivingAddress = await _generateAddressForChain(0,
newReceivingIndex); // Use new index to derive a new change address
await addToAddressesArrayForChain(newReceivingAddress,
0); // Add that new receiving address to the array of change addresses
}
} on SocketException catch (se, s) {
Logging.instance.log(
"SocketException caught in checkChangeAddressForTransactions(): $se\n$s",
level: LogLevel.Error);
return;
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from checkChangeAddressForTransactions(): $e\n$s",
level: LogLevel.Error); level: LogLevel.Error);
rethrow; rethrow;
} }