mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-16 17:27:39 +00:00
non lelantus firo sends
This commit is contained in:
parent
f8c8dcabfc
commit
15c4d0e617
4 changed files with 768 additions and 46 deletions
|
@ -9,6 +9,7 @@ import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
|||
import 'package:stackwallet/providers/providers.dart';
|
||||
import 'package:stackwallet/route_generator.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/enums/coin_enum.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/stack_dialog.dart';
|
||||
|
||||
import '../../providers/wallet/public_private_balance_state_provider.dart';
|
||||
|
||||
class ConfirmTransactionView extends ConsumerStatefulWidget {
|
||||
const ConfirmTransactionView({
|
||||
Key? key,
|
||||
|
@ -56,10 +59,20 @@ class _ConfirmTransactionViewState
|
|||
|
||||
final note = transactionInfo["note"] as String? ?? "";
|
||||
final manager =
|
||||
ref.read(walletsChangeNotifierProvider).getManager(walletId);
|
||||
ref.read(walletsChangeNotifierProvider).getManager(walletId);
|
||||
|
||||
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());
|
||||
|
||||
// save note
|
||||
|
@ -79,7 +92,7 @@ class _ConfirmTransactionViewState
|
|||
showFloatingFlushBar(
|
||||
type: FlushBarType.warning,
|
||||
message:
|
||||
"Connection failed. Please check the address and try again.",
|
||||
"Connection failed. Please check the address and try again.",
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
|
@ -100,10 +113,10 @@ class _ConfirmTransactionViewState
|
|||
message: e.toString(),
|
||||
rightButton: TextButton(
|
||||
style: Theme.of(context).textButtonTheme.style?.copyWith(
|
||||
backgroundColor: MaterialStateProperty.all<Color>(
|
||||
CFColors.buttonGray,
|
||||
),
|
||||
),
|
||||
backgroundColor: MaterialStateProperty.all<Color>(
|
||||
CFColors.buttonGray,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Ok",
|
||||
style: STextStyles.button.copyWith(
|
||||
|
@ -212,9 +225,9 @@ class _ConfirmTransactionViewState
|
|||
.select((value) => value.locale),
|
||||
),
|
||||
)} ${ref.watch(
|
||||
managerProvider
|
||||
.select((value) => value.coin),
|
||||
).ticker}",
|
||||
managerProvider
|
||||
.select((value) => value.coin),
|
||||
).ticker}",
|
||||
style: STextStyles.itemSubtitle12,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
|
@ -240,9 +253,9 @@ class _ConfirmTransactionViewState
|
|||
.select((value) => value.locale),
|
||||
),
|
||||
)} ${ref.watch(
|
||||
managerProvider
|
||||
.select((value) => value.coin),
|
||||
).ticker}",
|
||||
managerProvider
|
||||
.select((value) => value.coin),
|
||||
).ticker}",
|
||||
style: STextStyles.itemSubtitle12,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
|
@ -292,9 +305,9 @@ class _ConfirmTransactionViewState
|
|||
.select((value) => value.locale),
|
||||
),
|
||||
)} ${ref.watch(
|
||||
managerProvider
|
||||
.select((value) => value.coin),
|
||||
).ticker}",
|
||||
managerProvider
|
||||
.select((value) => value.coin),
|
||||
).ticker}",
|
||||
style: STextStyles.itemSubtitle12,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
|
@ -306,18 +319,18 @@ class _ConfirmTransactionViewState
|
|||
),
|
||||
TextButton(
|
||||
style:
|
||||
Theme.of(context).textButtonTheme.style?.copyWith(
|
||||
backgroundColor:
|
||||
MaterialStateProperty.all<Color>(
|
||||
CFColors.stackAccent,
|
||||
),
|
||||
),
|
||||
Theme.of(context).textButtonTheme.style?.copyWith(
|
||||
backgroundColor:
|
||||
MaterialStateProperty.all<Color>(
|
||||
CFColors.stackAccent,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
final unlocked = await Navigator.push(
|
||||
context,
|
||||
RouteGenerator.getRoute(
|
||||
shouldUseMaterialRoute:
|
||||
RouteGenerator.useMaterialPageRoute,
|
||||
RouteGenerator.useMaterialPageRoute,
|
||||
builder: (_) => const LockscreenView(
|
||||
showBackButton: true,
|
||||
popOnSuccess: true,
|
||||
|
@ -325,9 +338,9 @@ class _ConfirmTransactionViewState
|
|||
routeOnSuccess: "",
|
||||
biometricsCancelButtonString: "CANCEL",
|
||||
biometricsLocalizedReason:
|
||||
"Authenticate to send transaction",
|
||||
"Authenticate to send transaction",
|
||||
biometricsAuthenticationTitle:
|
||||
"Confirm Transaction",
|
||||
"Confirm Transaction",
|
||||
),
|
||||
settings: const RouteSettings(
|
||||
name: "/confirmsendlockscreen"),
|
||||
|
|
|
@ -1333,9 +1333,28 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
|
||||
final amount = Format.decimalAmountToSatoshis(
|
||||
_amountToSend!);
|
||||
final availableBalance =
|
||||
Format.decimalAmountToSatoshis(
|
||||
await manager.availableBalance);
|
||||
int availableBalance;
|
||||
if ((coin == Coin.firo ||
|
||||
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
|
||||
if (amount == availableBalance) {
|
||||
|
@ -1419,14 +1438,36 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
},
|
||||
));
|
||||
|
||||
final txData = await manager.prepareSend(
|
||||
address: _address!,
|
||||
satoshiAmount: amount,
|
||||
args: {
|
||||
"feeRate":
|
||||
ref.read(feeRateTypeStateProvider)
|
||||
},
|
||||
);
|
||||
Map<String, dynamic> txData;
|
||||
|
||||
if ((coin == Coin.firo ||
|
||||
coin == Coin.firoTestNet) &&
|
||||
ref
|
||||
.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) {
|
||||
// pop building dialog
|
||||
|
|
|
@ -10,7 +10,7 @@ class TxIcon extends StatelessWidget {
|
|||
static const Size size = Size(32, 32);
|
||||
|
||||
String _getAssetName(bool isCancelled, bool isReceived, bool isPending) {
|
||||
if (transaction.subType == "mint") {
|
||||
if (!isReceived && transaction.subType == "mint") {
|
||||
if (isCancelled) {
|
||||
return Assets.svg.anonymizeFailed;
|
||||
}
|
||||
|
|
|
@ -37,12 +37,14 @@ import 'package:stackwallet/utilities/assets.dart';
|
|||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/default_nodes.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/format.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
const DUST_LIMIT = 1000;
|
||||
const MINIMUM_CONFIRMATIONS = 1;
|
||||
const MINT_LIMIT = 100100000000;
|
||||
const int LELANTUS_VALUE_SPEND_LIMIT_PER_TRANSACTION = 5001 * 100000000;
|
||||
|
@ -973,11 +975,117 @@ class FiroWallet extends CoinServiceAPI {
|
|||
@override
|
||||
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
|
||||
Future<Map<String, dynamic>> prepareSend(
|
||||
{required String address,
|
||||
required int satoshiAmount,
|
||||
Map<String, dynamic>? args}) async {
|
||||
Future<Map<String, dynamic>> prepareSend({
|
||||
required String address,
|
||||
required int satoshiAmount,
|
||||
Map<String, dynamic>? args,
|
||||
}) async {
|
||||
try {
|
||||
dynamic txHexOrError =
|
||||
await _createJoinSplitTransaction(satoshiAmount, address, false);
|
||||
|
@ -1129,6 +1237,531 @@ class FiroWallet extends CoinServiceAPI {
|
|||
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
|
||||
Future<void> updateNode(bool shouldRefresh) async {
|
||||
final failovers = NodeService()
|
||||
|
@ -2140,9 +2773,11 @@ class FiroWallet extends CoinServiceAPI {
|
|||
!listLelantusTxData.containsKey(value.txid)) {
|
||||
// Every receive should be listed whether minted or not.
|
||||
listLelantusTxData[value.txid] = value;
|
||||
} else if (value.txType == "Sent" &&
|
||||
hasAtLeastOneReceive &&
|
||||
value.subType == "mint") {
|
||||
} else if (value.txType == "Sent"
|
||||
// &&
|
||||
// hasAtLeastOneReceive &&
|
||||
// value.subType == "mint"
|
||||
) {
|
||||
listLelantusTxData[value.txid] = value;
|
||||
|
||||
// use mint sends to update receives with user readable values.
|
||||
|
@ -2402,12 +3037,45 @@ class FiroWallet extends CoinServiceAPI {
|
|||
}
|
||||
} on SocketException catch (se, s) {
|
||||
Logging.instance.log(
|
||||
"SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s",
|
||||
"SocketException caught in checkReceivingAddressForTransactions(): $se\n$s",
|
||||
level: LogLevel.Error);
|
||||
return;
|
||||
} catch (e, s) {
|
||||
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);
|
||||
rethrow;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue