mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-03-09 10:15:22 +00:00
WIP notif txn
This commit is contained in:
parent
35a8172d35
commit
54767947ed
2 changed files with 269 additions and 19 deletions
lib/services/coins
|
@ -13,6 +13,20 @@ import 'package:stackwallet/models/paymint/utxo_model.dart';
|
||||||
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
|
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
|
||||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
import 'package:stackwallet/utilities/format.dart';
|
import 'package:stackwallet/utilities/format.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
class SWException with Exception {
|
||||||
|
SWException(this.message);
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => message;
|
||||||
|
}
|
||||||
|
|
||||||
|
class InsufficientBalanceException extends SWException {
|
||||||
|
InsufficientBalanceException(super.message);
|
||||||
|
}
|
||||||
|
|
||||||
extension PayNym on DogecoinWallet {
|
extension PayNym on DogecoinWallet {
|
||||||
// fetch or generate this wallet's bip47 payment code
|
// fetch or generate this wallet's bip47 payment code
|
||||||
|
@ -55,12 +69,187 @@ extension PayNym on DogecoinWallet {
|
||||||
// Future<Map<String, dynamic>> prepareNotificationTransaction(
|
// Future<Map<String, dynamic>> prepareNotificationTransaction(
|
||||||
// String targetPaymentCode) async {}
|
// String targetPaymentCode) async {}
|
||||||
|
|
||||||
Future<String> createNotificationTx(
|
Future<Map<String, dynamic>> buildNotificationTx({
|
||||||
String targetPaymentCodeString,
|
required int selectedTxFeeRate,
|
||||||
List<UtxoObject> utxosToUse,
|
required String targetPaymentCodeString,
|
||||||
int dustLimit,
|
int additionalOutputs = 0,
|
||||||
) async {
|
List<UtxoObject>? utxos,
|
||||||
final utxoSigningData = await fetchBuildTxData(utxosToUse);
|
}) async {
|
||||||
|
const amountToSend = DUST_LIMIT;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spendableSatoshiValue < amountToSend) {
|
||||||
|
// insufficient balance
|
||||||
|
throw InsufficientBalanceException(
|
||||||
|
"Spendable balance is less than the minimum required for a notification transaction.");
|
||||||
|
} else if (spendableSatoshiValue == amountToSend) {
|
||||||
|
// insufficient balance due to missing amount to cover fee
|
||||||
|
throw InsufficientBalanceException(
|
||||||
|
"Remaining balance does not cover the network fee.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort spendable by age (oldest first)
|
||||||
|
spendableOutputs.sort(
|
||||||
|
(a, b) => b.status.confirmations.compareTo(a.status.confirmations));
|
||||||
|
|
||||||
|
int satoshisBeingUsed = 0;
|
||||||
|
int outputsBeingUsed = 0;
|
||||||
|
List<UtxoObject> utxoObjectsToUse = [];
|
||||||
|
|
||||||
|
for (int i = 0;
|
||||||
|
satoshisBeingUsed < amountToSend && i < spendableOutputs.length;
|
||||||
|
i++) {
|
||||||
|
utxoObjectsToUse.add(spendableOutputs[i]);
|
||||||
|
satoshisBeingUsed += spendableOutputs[i].value;
|
||||||
|
outputsBeingUsed += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add additional outputs if required
|
||||||
|
for (int i = 0;
|
||||||
|
i < additionalOutputs && outputsBeingUsed < spendableOutputs.length;
|
||||||
|
i++) {
|
||||||
|
utxoObjectsToUse.add(spendableOutputs[outputsBeingUsed]);
|
||||||
|
satoshisBeingUsed += spendableOutputs[outputsBeingUsed].value;
|
||||||
|
outputsBeingUsed += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// gather required signing data
|
||||||
|
final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse);
|
||||||
|
|
||||||
|
final int vSizeForNoChange = (await _createNotificationTx(
|
||||||
|
targetPaymentCodeString: targetPaymentCodeString,
|
||||||
|
utxosToUse: utxoObjectsToUse,
|
||||||
|
utxoSigningData: utxoSigningData,
|
||||||
|
change: 0))
|
||||||
|
.item2;
|
||||||
|
|
||||||
|
final int vSizeForWithChange = (await _createNotificationTx(
|
||||||
|
targetPaymentCodeString: targetPaymentCodeString,
|
||||||
|
utxosToUse: utxoObjectsToUse,
|
||||||
|
utxoSigningData: utxoSigningData,
|
||||||
|
change: satoshisBeingUsed - amountToSend))
|
||||||
|
.item2;
|
||||||
|
|
||||||
|
// Assume 2 outputs, for recipient and payment code script
|
||||||
|
int feeForNoChange = estimateTxFee(
|
||||||
|
vSize: vSizeForNoChange,
|
||||||
|
feeRatePerKB: selectedTxFeeRate,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assume 3 outputs, for recipient, payment code script, and change
|
||||||
|
int feeForWithChange = estimateTxFee(
|
||||||
|
vSize: vSizeForWithChange,
|
||||||
|
feeRatePerKB: selectedTxFeeRate,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (feeForNoChange < vSizeForNoChange * 1000) {
|
||||||
|
feeForNoChange = vSizeForNoChange * 1000;
|
||||||
|
}
|
||||||
|
if (feeForWithChange < vSizeForWithChange * 1000) {
|
||||||
|
feeForWithChange = vSizeForWithChange * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (satoshisBeingUsed - amountToSend > feeForNoChange + DUST_LIMIT) {
|
||||||
|
// try to add change output due to "left over" amount being greater than
|
||||||
|
// the estimated fee + the dust limit
|
||||||
|
int changeAmount = satoshisBeingUsed - amountToSend - feeForWithChange;
|
||||||
|
|
||||||
|
// check estimates are correct and build notification tx
|
||||||
|
if (changeAmount >= DUST_LIMIT &&
|
||||||
|
satoshisBeingUsed - amountToSend - changeAmount == feeForWithChange) {
|
||||||
|
final txn = await _createNotificationTx(
|
||||||
|
targetPaymentCodeString: targetPaymentCodeString,
|
||||||
|
utxosToUse: utxoObjectsToUse,
|
||||||
|
utxoSigningData: utxoSigningData,
|
||||||
|
change: changeAmount,
|
||||||
|
);
|
||||||
|
|
||||||
|
int feeBeingPaid = satoshisBeingUsed - amountToSend - changeAmount;
|
||||||
|
|
||||||
|
Map<String, dynamic> transactionObject = {
|
||||||
|
"hex": txn.item1,
|
||||||
|
"recipientPaynym": targetPaymentCodeString,
|
||||||
|
// "recipientAmt": recipientsAmtArray[0],
|
||||||
|
"fee": feeBeingPaid,
|
||||||
|
"vSize": txn.item2,
|
||||||
|
};
|
||||||
|
return transactionObject;
|
||||||
|
} else {
|
||||||
|
// something broke during fee estimation or the change amount is smaller
|
||||||
|
// than the dust limit. Try without change
|
||||||
|
final txn = await _createNotificationTx(
|
||||||
|
targetPaymentCodeString: targetPaymentCodeString,
|
||||||
|
utxosToUse: utxoObjectsToUse,
|
||||||
|
utxoSigningData: utxoSigningData,
|
||||||
|
change: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
int feeBeingPaid = satoshisBeingUsed - amountToSend;
|
||||||
|
|
||||||
|
Map<String, dynamic> transactionObject = {
|
||||||
|
"hex": txn.item1,
|
||||||
|
"recipientPaynym": targetPaymentCodeString,
|
||||||
|
// "recipientAmt": recipientsAmtArray[0],
|
||||||
|
"fee": feeBeingPaid,
|
||||||
|
"vSize": txn.item2,
|
||||||
|
};
|
||||||
|
return transactionObject;
|
||||||
|
}
|
||||||
|
} else if (satoshisBeingUsed - amountToSend >= feeForNoChange) {
|
||||||
|
// since we already checked if we need to add a change output we can just
|
||||||
|
// build without change here
|
||||||
|
final txn = await _createNotificationTx(
|
||||||
|
targetPaymentCodeString: targetPaymentCodeString,
|
||||||
|
utxosToUse: utxoObjectsToUse,
|
||||||
|
utxoSigningData: utxoSigningData,
|
||||||
|
change: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
int feeBeingPaid = satoshisBeingUsed - amountToSend;
|
||||||
|
|
||||||
|
Map<String, dynamic> transactionObject = {
|
||||||
|
"hex": txn.item1,
|
||||||
|
"recipientPaynym": targetPaymentCodeString,
|
||||||
|
// "recipientAmt": recipientsAmtArray[0],
|
||||||
|
"fee": feeBeingPaid,
|
||||||
|
"vSize": txn.item2,
|
||||||
|
};
|
||||||
|
return transactionObject;
|
||||||
|
} else {
|
||||||
|
// if we get here we do not have enough funds to cover the tx total so we
|
||||||
|
// check if we have any more available outputs and try again
|
||||||
|
if (spendableOutputs.length > outputsBeingUsed) {
|
||||||
|
return buildNotificationTx(
|
||||||
|
selectedTxFeeRate: selectedTxFeeRate,
|
||||||
|
targetPaymentCodeString: targetPaymentCodeString,
|
||||||
|
additionalOutputs: additionalOutputs + 1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw InsufficientBalanceException(
|
||||||
|
"Remaining balance does not cover the network fee.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return tuple with string value equal to the raw tx hex and the int value
|
||||||
|
// equal to its vSize
|
||||||
|
Future<Tuple2<String, int>> _createNotificationTx({
|
||||||
|
required String targetPaymentCodeString,
|
||||||
|
required List<UtxoObject> utxosToUse,
|
||||||
|
required Map<String, dynamic> utxoSigningData,
|
||||||
|
required int change,
|
||||||
|
}) async {
|
||||||
final targetPaymentCode =
|
final targetPaymentCode =
|
||||||
PaymentCode.fromPaymentCode(targetPaymentCodeString, network);
|
PaymentCode.fromPaymentCode(targetPaymentCodeString, network);
|
||||||
final myCode = await getPaymentCode();
|
final myCode = await getPaymentCode();
|
||||||
|
@ -109,19 +298,80 @@ extension PayNym on DogecoinWallet {
|
||||||
txPointIndex,
|
txPointIndex,
|
||||||
);
|
);
|
||||||
|
|
||||||
txb.addOutput(targetPaymentCode.notificationAddress(), dustLimit);
|
txb.addOutput(targetPaymentCode.notificationAddress(), DUST_LIMIT);
|
||||||
txb.addOutput(opReturnScript, 0);
|
txb.addOutput(opReturnScript, 0);
|
||||||
|
|
||||||
// TODO: add possible change output and mark output as dangerous
|
// TODO: add possible change output and mark output as dangerous
|
||||||
|
if (change > 0) {
|
||||||
|
// generate new change address if current change address has been used
|
||||||
|
await checkChangeAddressForTransactions(DerivePathType.bip44);
|
||||||
|
final String changeAddress =
|
||||||
|
await getCurrentAddressForChain(1, DerivePathType.bip44);
|
||||||
|
txb.addOutput(changeAddress, change);
|
||||||
|
}
|
||||||
|
|
||||||
txb.sign(
|
txb.sign(
|
||||||
vin: 0,
|
vin: 0,
|
||||||
keyPair: myKeyPair,
|
keyPair: myKeyPair,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// sign rest of possible inputs
|
||||||
|
for (var i = 1; i < utxosToUse.length - 1; i++) {
|
||||||
|
final txid = utxosToUse[i].txid;
|
||||||
|
txb.sign(
|
||||||
|
vin: i,
|
||||||
|
keyPair: utxoSigningData[txid]["keyPair"] as ECPair,
|
||||||
|
// witnessValue: utxosToUse[i].value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final builtTx = txb.build();
|
final builtTx = txb.build();
|
||||||
|
|
||||||
return builtTx.toHex();
|
return Tuple2(builtTx.toHex(), builtTx.virtualSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> hasConfirmedNotificationTxSentTo(
|
||||||
|
String paymentCodeString) async {
|
||||||
|
final targetPaymentCode =
|
||||||
|
PaymentCode.fromPaymentCode(paymentCodeString, network);
|
||||||
|
final targetNotificationAddress = targetPaymentCode.notificationAddress();
|
||||||
|
|
||||||
|
final myTxHistory = (await transactionData)
|
||||||
|
.getAllTransactions()
|
||||||
|
.entries
|
||||||
|
.map((e) => e.value)
|
||||||
|
.where((e) =>
|
||||||
|
e.txType == "Sent" && e.address == targetNotificationAddress);
|
||||||
|
|
||||||
|
return myTxHistory.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch paynym notification tx meta data
|
||||||
|
Set<Map<String, dynamic>> getPaynymNotificationTxInfo() {
|
||||||
|
final set = DB.instance.get<dynamic>(
|
||||||
|
boxName: walletId, key: "paynymNotificationTxInfo") as Set? ??
|
||||||
|
{};
|
||||||
|
|
||||||
|
return Set<Map<String, dynamic>>.from(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add/update paynym notification tx meta data entry
|
||||||
|
Future<void> updatePaynymNotificationInfo({
|
||||||
|
required String txid,
|
||||||
|
required bool confirmed,
|
||||||
|
required String paymentCodeString,
|
||||||
|
}) async {
|
||||||
|
final data = getPaynymNotificationTxInfo();
|
||||||
|
data.add({
|
||||||
|
"txid": txid,
|
||||||
|
"confirmed": confirmed,
|
||||||
|
"paymentCodeString": paymentCodeString,
|
||||||
|
});
|
||||||
|
await DB.instance.put<dynamic>(
|
||||||
|
boxName: walletId,
|
||||||
|
key: "paynymNotificationTxInfo",
|
||||||
|
value: data,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -204,7 +204,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
||||||
@override
|
@override
|
||||||
Future<String> get currentReceivingAddress =>
|
Future<String> get currentReceivingAddress =>
|
||||||
_currentReceivingAddressP2PKH ??=
|
_currentReceivingAddressP2PKH ??=
|
||||||
_getCurrentAddressForChain(0, DerivePathType.bip44);
|
getCurrentAddressForChain(0, DerivePathType.bip44);
|
||||||
|
|
||||||
Future<String>? _currentReceivingAddressP2PKH;
|
Future<String>? _currentReceivingAddressP2PKH;
|
||||||
|
|
||||||
|
@ -776,7 +776,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId));
|
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId));
|
||||||
await _checkChangeAddressForTransactions(DerivePathType.bip44);
|
await checkChangeAddressForTransactions(DerivePathType.bip44);
|
||||||
|
|
||||||
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
|
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
|
||||||
await _checkCurrentReceivingAddressesForTransactions();
|
await _checkCurrentReceivingAddressesForTransactions();
|
||||||
|
@ -1446,7 +1446,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
||||||
/// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain]
|
/// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain]
|
||||||
/// and
|
/// and
|
||||||
/// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
|
/// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
|
||||||
Future<String> _getCurrentAddressForChain(
|
Future<String> getCurrentAddressForChain(
|
||||||
int chain, DerivePathType derivePathType) async {
|
int chain, DerivePathType derivePathType) async {
|
||||||
// Here, we assume that chain == 1 if it isn't 0
|
// Here, we assume that chain == 1 if it isn't 0
|
||||||
String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses";
|
String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses";
|
||||||
|
@ -1801,7 +1801,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
||||||
DerivePathType derivePathType) async {
|
DerivePathType derivePathType) async {
|
||||||
try {
|
try {
|
||||||
final String currentExternalAddr =
|
final String currentExternalAddr =
|
||||||
await _getCurrentAddressForChain(0, derivePathType);
|
await getCurrentAddressForChain(0, derivePathType);
|
||||||
final int txCount = await getTxCount(address: currentExternalAddr);
|
final int txCount = await getTxCount(address: currentExternalAddr);
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
'Number of txs for current receiving address $currentExternalAddr: $txCount',
|
'Number of txs for current receiving address $currentExternalAddr: $txCount',
|
||||||
|
@ -1850,11 +1850,11 @@ class DogecoinWallet extends CoinServiceAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkChangeAddressForTransactions(
|
Future<void> checkChangeAddressForTransactions(
|
||||||
DerivePathType derivePathType) async {
|
DerivePathType derivePathType) async {
|
||||||
try {
|
try {
|
||||||
final String currentExternalAddr =
|
final String currentExternalAddr =
|
||||||
await _getCurrentAddressForChain(1, derivePathType);
|
await getCurrentAddressForChain(1, derivePathType);
|
||||||
final int txCount = await getTxCount(address: currentExternalAddr);
|
final int txCount = await getTxCount(address: currentExternalAddr);
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
'Number of txs for current change address $currentExternalAddr: $txCount',
|
'Number of txs for current change address $currentExternalAddr: $txCount',
|
||||||
|
@ -1916,7 +1916,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
||||||
Future<void> _checkCurrentChangeAddressesForTransactions() async {
|
Future<void> _checkCurrentChangeAddressesForTransactions() async {
|
||||||
try {
|
try {
|
||||||
for (final type in DerivePathType.values) {
|
for (final type in DerivePathType.values) {
|
||||||
await _checkChangeAddressForTransactions(type);
|
await checkChangeAddressForTransactions(type);
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
|
@ -2480,7 +2480,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
||||||
utxoSigningData: utxoSigningData,
|
utxoSigningData: utxoSigningData,
|
||||||
recipients: [
|
recipients: [
|
||||||
_recipientAddress,
|
_recipientAddress,
|
||||||
await _getCurrentAddressForChain(1, DerivePathType.bip44),
|
await getCurrentAddressForChain(1, DerivePathType.bip44),
|
||||||
],
|
],
|
||||||
satoshiAmounts: [
|
satoshiAmounts: [
|
||||||
satoshiAmountToSend,
|
satoshiAmountToSend,
|
||||||
|
@ -2532,9 +2532,9 @@ class DogecoinWallet extends CoinServiceAPI {
|
||||||
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize ==
|
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize ==
|
||||||
feeForTwoOutputs) {
|
feeForTwoOutputs) {
|
||||||
// generate new change address if current change address has been used
|
// generate new change address if current change address has been used
|
||||||
await _checkChangeAddressForTransactions(DerivePathType.bip44);
|
await checkChangeAddressForTransactions(DerivePathType.bip44);
|
||||||
final String newChangeAddress =
|
final String newChangeAddress =
|
||||||
await _getCurrentAddressForChain(1, DerivePathType.bip44);
|
await getCurrentAddressForChain(1, DerivePathType.bip44);
|
||||||
|
|
||||||
int feeBeingPaid =
|
int feeBeingPaid =
|
||||||
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
|
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
|
||||||
|
@ -2685,7 +2685,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
||||||
};
|
};
|
||||||
return transactionObject;
|
return transactionObject;
|
||||||
} else {
|
} else {
|
||||||
// Remember that returning 2 indicates that the user does not have a sufficient balance to
|
// Remember that returning 2 iTndicates 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
|
// 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.
|
// additional outputs they're able to spend and then recalculate fees.
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
|
|
Loading…
Reference in a new issue