mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-17 01:37:54 +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/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"),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue