mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-22 10:34:32 +00:00
commit
43c77a43a4
5 changed files with 553 additions and 336 deletions
|
@ -39,6 +39,7 @@ class _StackThemeCardState extends ConsumerState<StackThemeCard> {
|
|||
late final StreamSubscription<void> _subscription;
|
||||
|
||||
late bool _hasTheme;
|
||||
bool _needsUpdate = false;
|
||||
String? _cachedSize;
|
||||
|
||||
Future<bool> _downloadAndInstall() async {
|
||||
|
@ -84,6 +85,7 @@ class _StackThemeCardState extends ConsumerState<StackThemeCard> {
|
|||
title: message,
|
||||
onOkPressed: (_) {
|
||||
setState(() {
|
||||
_needsUpdate = !result;
|
||||
_hasTheme = result;
|
||||
});
|
||||
},
|
||||
|
@ -141,16 +143,18 @@ class _StackThemeCardState extends ConsumerState<StackThemeCard> {
|
|||
}
|
||||
}
|
||||
|
||||
StackTheme? getInstalled() => ref
|
||||
.read(mainDBProvider)
|
||||
.isar
|
||||
.stackThemes
|
||||
.where()
|
||||
.themeIdEqualTo(widget.data.id)
|
||||
.findFirstSync();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_hasTheme = ref
|
||||
.read(mainDBProvider)
|
||||
.isar
|
||||
.stackThemes
|
||||
.where()
|
||||
.themeIdEqualTo(widget.data.id)
|
||||
.countSync() >
|
||||
0;
|
||||
final installedTheme = getInstalled();
|
||||
_hasTheme = installedTheme != null;
|
||||
|
||||
_subscription = ref
|
||||
.read(mainDBProvider)
|
||||
|
@ -158,18 +162,15 @@ class _StackThemeCardState extends ConsumerState<StackThemeCard> {
|
|||
.stackThemes
|
||||
.watchLazy()
|
||||
.listen((event) async {
|
||||
final hasTheme = (await ref
|
||||
.read(mainDBProvider)
|
||||
.isar
|
||||
.stackThemes
|
||||
.where()
|
||||
.themeIdEqualTo(widget.data.id)
|
||||
.count()) >
|
||||
0;
|
||||
final installedTheme = getInstalled();
|
||||
final hasTheme = installedTheme != null;
|
||||
if (_hasTheme != hasTheme && mounted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
setState(() {
|
||||
_hasTheme = hasTheme;
|
||||
if (hasTheme) {
|
||||
_needsUpdate = widget.data.version > installedTheme.version!;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -272,6 +273,16 @@ class _StackThemeCardState extends ConsumerState<StackThemeCard> {
|
|||
}
|
||||
},
|
||||
),
|
||||
if (_hasTheme && _needsUpdate)
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
if (_hasTheme && _needsUpdate)
|
||||
PrimaryButton(
|
||||
label: "Update",
|
||||
buttonHeight: isDesktop ? ButtonHeight.s : ButtonHeight.l,
|
||||
onPressed: _downloadPressed,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
|
|
|
@ -619,16 +619,37 @@ class _TransactionDetailsViewState
|
|||
CustomTextButton(
|
||||
text: "Info",
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
.pushNamed(
|
||||
AddressDetailsView
|
||||
.routeName,
|
||||
arguments: Tuple2(
|
||||
_transaction.address
|
||||
.value!.id,
|
||||
widget.walletId,
|
||||
),
|
||||
);
|
||||
if (isDesktop) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
DesktopDialog(
|
||||
maxHeight:
|
||||
double.infinity,
|
||||
child:
|
||||
AddressDetailsView(
|
||||
addressId:
|
||||
_transaction
|
||||
.address
|
||||
.value!
|
||||
.id,
|
||||
walletId: widget
|
||||
.walletId,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.of(context)
|
||||
.pushNamed(
|
||||
AddressDetailsView
|
||||
.routeName,
|
||||
arguments: Tuple2(
|
||||
_transaction.address
|
||||
.value!.id,
|
||||
widget.walletId,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
|
@ -1129,6 +1150,46 @@ class _TransactionDetailsViewState
|
|||
],
|
||||
),
|
||||
),
|
||||
if (kDebugMode)
|
||||
isDesktop
|
||||
? const _Divider()
|
||||
: const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
if (kDebugMode)
|
||||
RoundedWhiteContainer(
|
||||
padding: isDesktop
|
||||
? const EdgeInsets.all(16)
|
||||
: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Tx sub type",
|
||||
style: isDesktop
|
||||
? STextStyles
|
||||
.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
: STextStyles.itemSubtitle(context),
|
||||
),
|
||||
SelectableText(
|
||||
_transaction.subType.toString(),
|
||||
style: isDesktop
|
||||
? STextStyles
|
||||
.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark,
|
||||
)
|
||||
: STextStyles.itemSubtitle12(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
isDesktop
|
||||
? const _Divider()
|
||||
: const SizedBox(
|
||||
|
|
|
@ -405,7 +405,7 @@ Future<Map<dynamic, dynamic>> staticProcessRestore(
|
|||
tx = null;
|
||||
}
|
||||
|
||||
if (tx == null) {
|
||||
if (tx == null || tx.subType == isar_models.TransactionSubType.join) {
|
||||
// This is a jmint.
|
||||
return;
|
||||
}
|
||||
|
@ -1930,12 +1930,13 @@ class FiroWallet extends CoinServiceAPI
|
|||
if ((await db
|
||||
.getTransactions(walletId)
|
||||
.filter()
|
||||
.txidMatches(txid)
|
||||
.findFirst()) ==
|
||||
null) {
|
||||
.txidEqualTo(txid)
|
||||
.count()) ==
|
||||
0) {
|
||||
Logging.instance.log(
|
||||
" txid not found in address history already ${transaction['tx_hash']}",
|
||||
level: LogLevel.Info);
|
||||
" txid not found in address history already ${transaction['tx_hash']}",
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
needsRefresh = true;
|
||||
break;
|
||||
}
|
||||
|
@ -1958,79 +1959,27 @@ class FiroWallet extends CoinServiceAPI
|
|||
|
||||
final currentChainHeight = await chainHeight;
|
||||
|
||||
final txTxns = await db
|
||||
.getTransactions(walletId)
|
||||
.filter()
|
||||
.isLelantusIsNull()
|
||||
.or()
|
||||
.isLelantusEqualTo(false)
|
||||
.findAll();
|
||||
final ltxTxns = await db
|
||||
.getTransactions(walletId)
|
||||
.filter()
|
||||
.isLelantusEqualTo(true)
|
||||
.findAll();
|
||||
final txCount = await db.getTransactions(walletId).count();
|
||||
|
||||
for (isar_models.Transaction tx in txTxns) {
|
||||
isar_models.Transaction? lTx;
|
||||
try {
|
||||
lTx = ltxTxns.firstWhere((e) => e.txid == tx.txid);
|
||||
} catch (_) {
|
||||
lTx = null;
|
||||
}
|
||||
const paginateLimit = 50;
|
||||
|
||||
if (tx.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
|
||||
if (txTracker.wasNotifiedPending(tx.txid) &&
|
||||
!txTracker.wasNotifiedConfirmed(tx.txid)) {
|
||||
for (int i = 0; i < txCount; i += paginateLimit) {
|
||||
final transactions = await db
|
||||
.getTransactions(walletId)
|
||||
.offset(i)
|
||||
.limit(paginateLimit)
|
||||
.findAll();
|
||||
for (final tx in transactions) {
|
||||
if (tx.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
|
||||
// get all transactions that were notified as pending but not as confirmed
|
||||
unconfirmedTxnsToNotifyConfirmed.add(tx);
|
||||
}
|
||||
if (lTx != null &&
|
||||
(lTx.inputs.isEmpty || lTx.inputs.first.txid.isEmpty) &&
|
||||
lTx.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
|
||||
false &&
|
||||
tx.type == isar_models.TransactionType.incoming) {
|
||||
// If this is a received that is past 1 or more confirmations and has not been minted,
|
||||
if (!txTracker.wasNotifiedPending(tx.txid)) {
|
||||
unconfirmedTxnsToNotifyPending.add(tx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!txTracker.wasNotifiedPending(tx.txid)) {
|
||||
// get all transactions that were not notified as pending yet
|
||||
unconfirmedTxnsToNotifyPending.add(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (isar_models.Transaction tx in txTxns) {
|
||||
if (!tx.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) &&
|
||||
tx.inputs.first.txid.isNotEmpty) {
|
||||
// Get all normal txs that are at 0 confirmations
|
||||
unconfirmedTxnsToNotifyPending
|
||||
.removeWhere((e) => e.txid == tx.inputs.first.txid);
|
||||
Logging.instance.log("removed tx: ${tx.txid}", level: LogLevel.Info);
|
||||
}
|
||||
}
|
||||
|
||||
for (isar_models.Transaction lTX in ltxTxns) {
|
||||
isar_models.Transaction? tx;
|
||||
try {
|
||||
tx = ltxTxns.firstWhere((e) => e.txid == lTX.txid);
|
||||
} catch (_) {
|
||||
tx = null;
|
||||
}
|
||||
|
||||
if (tx == null) {
|
||||
// if this is a ltx transaction that is unconfirmed and not represented in the normal transaction set.
|
||||
if (!lTX.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
|
||||
if (!txTracker.wasNotifiedPending(lTX.txid)) {
|
||||
unconfirmedTxnsToNotifyPending.add(lTX);
|
||||
if (txTracker.wasNotifiedPending(tx.txid) &&
|
||||
!txTracker.wasNotifiedConfirmed(tx.txid)) {
|
||||
unconfirmedTxnsToNotifyConfirmed.add(tx);
|
||||
}
|
||||
} else {
|
||||
if (txTracker.wasNotifiedPending(lTX.txid) &&
|
||||
!txTracker.wasNotifiedConfirmed(lTX.txid)) {
|
||||
unconfirmedTxnsToNotifyConfirmed.add(lTX);
|
||||
// get all transactions that were not notified as pending yet
|
||||
if (!txTracker.wasNotifiedPending(tx.txid)) {
|
||||
unconfirmedTxnsToNotifyPending.add(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2606,7 +2555,7 @@ class FiroWallet extends CoinServiceAPI
|
|||
var tmpTotal = total;
|
||||
var index = 1;
|
||||
var mints = <Map<String, dynamic>>[];
|
||||
final nextFreeMintIndex = firoGetMintIndex()!;
|
||||
final nextFreeMintIndex = firoGetMintIndex();
|
||||
while (tmpTotal > 0) {
|
||||
final mintValue = min(tmpTotal, MINT_LIMIT);
|
||||
final mint = await _getMintHex(
|
||||
|
@ -2771,7 +2720,7 @@ class FiroWallet extends CoinServiceAPI
|
|||
amount += utxosToUse[i].value;
|
||||
}
|
||||
|
||||
final index = firoGetMintIndex()!;
|
||||
final index = firoGetMintIndex();
|
||||
Logging.instance.log("index of mint $index", level: LogLevel.Info);
|
||||
|
||||
for (var mintsElement in mintsMap) {
|
||||
|
@ -3038,7 +2987,7 @@ class FiroWallet extends CoinServiceAPI
|
|||
|
||||
// if a jmint was made add it to the unspent coin index
|
||||
LelantusCoin jmint = LelantusCoin(
|
||||
index!,
|
||||
index,
|
||||
transactionInfo['jmintValue'] as int? ?? 0,
|
||||
transactionInfo['publicCoin'] as String,
|
||||
transactionInfo['txid'] as String,
|
||||
|
@ -3215,17 +3164,18 @@ class FiroWallet extends CoinServiceAPI
|
|||
);
|
||||
}
|
||||
|
||||
//TODO call get transaction and check each tx to see if it is a "received" tx?
|
||||
Future<int> _getReceivedTxCount({required String address}) async {
|
||||
Future<int> _getTxCount({required String address}) async {
|
||||
try {
|
||||
final scripthash = AddressUtils.convertToScriptHash(address, _network);
|
||||
final transactions =
|
||||
await electrumXClient.getHistory(scripthash: scripthash);
|
||||
final scriptHash = AddressUtils.convertToScriptHash(address, _network);
|
||||
final transactions = await electrumXClient.getHistory(
|
||||
scripthash: scriptHash,
|
||||
);
|
||||
return transactions.length;
|
||||
} catch (e) {
|
||||
Logging.instance.log(
|
||||
"Exception rethrown in _getReceivedTxCount(address: $address): $e",
|
||||
level: LogLevel.Error);
|
||||
"Exception rethrown in _getReceivedTxCount(address: $address): $e",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
@ -3234,8 +3184,7 @@ class FiroWallet extends CoinServiceAPI
|
|||
try {
|
||||
final currentReceiving = await _currentReceivingAddress;
|
||||
|
||||
final int txCount =
|
||||
await _getReceivedTxCount(address: currentReceiving.value);
|
||||
final int txCount = await _getTxCount(address: currentReceiving.value);
|
||||
Logging.instance.log(
|
||||
'Number of txs for current receiving address $currentReceiving: $txCount',
|
||||
level: LogLevel.Info);
|
||||
|
@ -3281,8 +3230,7 @@ class FiroWallet extends CoinServiceAPI
|
|||
Future<void> checkChangeAddressForTransactions() async {
|
||||
try {
|
||||
final currentChange = await _currentChangeAddress;
|
||||
final int txCount =
|
||||
await _getReceivedTxCount(address: currentChange.value);
|
||||
final int txCount = await _getTxCount(address: currentChange.value);
|
||||
Logging.instance.log(
|
||||
'Number of txs for current change address: $currentChange: $txCount',
|
||||
level: LogLevel.Info);
|
||||
|
@ -3330,27 +3278,13 @@ class FiroWallet extends CoinServiceAPI
|
|||
.getAddresses(walletId)
|
||||
.filter()
|
||||
.not()
|
||||
.typeEqualTo(isar_models.AddressType.nonWallet)
|
||||
.and()
|
||||
.group((q) => q
|
||||
.subTypeEqualTo(isar_models.AddressSubType.receiving)
|
||||
.or()
|
||||
.subTypeEqualTo(isar_models.AddressSubType.change))
|
||||
.group(
|
||||
(q) => q
|
||||
.typeEqualTo(isar_models.AddressType.nonWallet)
|
||||
.or()
|
||||
.subTypeEqualTo(isar_models.AddressSubType.nonWallet),
|
||||
)
|
||||
.findAll();
|
||||
// final List<String> allAddresses = [];
|
||||
// final receivingAddresses =
|
||||
// DB.instance.get<dynamic>(boxName: walletId, key: 'receivingAddresses')
|
||||
// as List<dynamic>;
|
||||
// final changeAddresses =
|
||||
// DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddresses')
|
||||
// as List<dynamic>;
|
||||
//
|
||||
// for (var i = 0; i < receivingAddresses.length; i++) {
|
||||
// allAddresses.add(receivingAddresses[i] as String);
|
||||
// }
|
||||
// for (var i = 0; i < changeAddresses.length; i++) {
|
||||
// allAddresses.add(changeAddresses[i] as String);
|
||||
// }
|
||||
return allAddresses;
|
||||
}
|
||||
|
||||
|
@ -3413,6 +3347,15 @@ class FiroWallet extends CoinServiceAPI
|
|||
final List<isar_models.Address> allAddresses =
|
||||
await _fetchAllOwnAddresses();
|
||||
|
||||
Set<String> receivingAddresses = allAddresses
|
||||
.where((e) => e.subType == isar_models.AddressSubType.receiving)
|
||||
.map((e) => e.value)
|
||||
.toSet();
|
||||
Set<String> changeAddresses = allAddresses
|
||||
.where((e) => e.subType == isar_models.AddressSubType.change)
|
||||
.map((e) => e.value)
|
||||
.toSet();
|
||||
|
||||
final List<Map<String, dynamic>> allTxHashes =
|
||||
await _fetchHistory(allAddresses.map((e) => e.value).toList());
|
||||
|
||||
|
@ -3450,215 +3393,405 @@ class FiroWallet extends CoinServiceAPI
|
|||
final List<Tuple2<isar_models.Transaction, isar_models.Address?>> txnsData =
|
||||
[];
|
||||
|
||||
Set<String> changeAddresses = allAddresses
|
||||
.where((e) => e.subType == isar_models.AddressSubType.change)
|
||||
.map((e) => e.value)
|
||||
.toSet();
|
||||
|
||||
for (final txObject in allTransactions) {
|
||||
// Logging.instance.log(txObject);
|
||||
List<String> sendersArray = [];
|
||||
List<String> recipientsArray = [];
|
||||
final inputList = txObject["vin"] as List;
|
||||
final outputList = txObject["vout"] as List;
|
||||
|
||||
// Usually only has value when txType = 'Send'
|
||||
int inputAmtSentFromWallet = 0;
|
||||
// Usually has value regardless of txType due to change addresses
|
||||
int outputAmtAddressedToWallet = 0;
|
||||
bool isMint = false;
|
||||
bool isJMint = false;
|
||||
|
||||
for (final input in txObject["vin"] as List) {
|
||||
final address = input["address"] as String?;
|
||||
if (address != null) {
|
||||
sendersArray.add(address);
|
||||
}
|
||||
}
|
||||
|
||||
// Logging.instance.log("sendersArray: $sendersArray");
|
||||
|
||||
for (final output in txObject["vout"] as List) {
|
||||
final address = output["scriptPubKey"]?["addresses"]?[0] as String? ??
|
||||
output["scriptPubKey"]?["address"] as String?;
|
||||
if (address != null) {
|
||||
recipientsArray.add(address);
|
||||
}
|
||||
}
|
||||
// Logging.instance.log("recipientsArray: $recipientsArray");
|
||||
|
||||
final foundInSenders =
|
||||
allAddresses.any((element) => sendersArray.contains(element.value));
|
||||
// Logging.instance.log("foundInSenders: $foundInSenders");
|
||||
|
||||
String outAddress = "";
|
||||
|
||||
int fees = 0;
|
||||
|
||||
// If txType = Sent, then calculate inputAmtSentFromWallet, calculate who received how much in aliens array (check outputs)
|
||||
if (foundInSenders) {
|
||||
int outAmount = 0;
|
||||
int inAmount = 0;
|
||||
bool nFeesUsed = false;
|
||||
|
||||
for (final input in txObject["vin"] as List) {
|
||||
final nFees = input["nFees"];
|
||||
if (nFees != null) {
|
||||
nFeesUsed = true;
|
||||
fees = (Decimal.parse(nFees.toString()) *
|
||||
Decimal.fromInt(Constants.satsPerCoin(coin)))
|
||||
.toBigInt()
|
||||
.toInt();
|
||||
}
|
||||
final address = input["address"] as String?;
|
||||
final value = input["valueSat"] as int?;
|
||||
if (address != null && value != null) {
|
||||
if (allAddresses.where((e) => e.value == address).isNotEmpty) {
|
||||
inputAmtSentFromWallet += value;
|
||||
}
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
inAmount += value;
|
||||
}
|
||||
}
|
||||
|
||||
for (final output in txObject["vout"] as List) {
|
||||
final address = output["scriptPubKey"]?["addresses"]?[0] as String? ??
|
||||
output["scriptPubKey"]?["address"] as String?;
|
||||
final value = output["value"];
|
||||
|
||||
if (value != null) {
|
||||
outAmount += (Decimal.parse(value.toString()) *
|
||||
Decimal.fromInt(Constants.satsPerCoin(coin)))
|
||||
.toBigInt()
|
||||
.toInt();
|
||||
|
||||
if (address != null) {
|
||||
if (changeAddresses.contains(address)) {
|
||||
inputAmtSentFromWallet -= (Decimal.parse(value.toString()) *
|
||||
Decimal.fromInt(Constants.satsPerCoin(coin)))
|
||||
.toBigInt()
|
||||
.toInt();
|
||||
} else {
|
||||
outAddress = address;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fees = nFeesUsed ? fees : inAmount - outAmount;
|
||||
inputAmtSentFromWallet -= inAmount - outAmount;
|
||||
} else {
|
||||
for (final input in txObject["vin"] as List) {
|
||||
final nFees = input["nFees"];
|
||||
if (nFees != null) {
|
||||
fees += (Decimal.parse(nFees.toString()) *
|
||||
Decimal.fromInt(Constants.satsPerCoin(coin)))
|
||||
.toBigInt()
|
||||
.toInt();
|
||||
}
|
||||
}
|
||||
|
||||
for (final output in txObject["vout"] as List) {
|
||||
final addresses = output["scriptPubKey"]["addresses"] as List?;
|
||||
if (addresses != null && addresses.isNotEmpty) {
|
||||
final address = addresses[0] as String;
|
||||
final value = output["value"] ?? 0;
|
||||
// Logging.instance.log(address + value.toString());
|
||||
|
||||
if (allAddresses.where((e) => e.value == address).isNotEmpty) {
|
||||
outputAmtAddressedToWallet += (Decimal.parse(value.toString()) *
|
||||
Decimal.fromInt(Constants.satsPerCoin(coin)))
|
||||
.toBigInt()
|
||||
.toInt();
|
||||
outAddress = address;
|
||||
// check if tx is Mint or jMint
|
||||
for (final output in outputList) {
|
||||
if (output["scriptPubKey"]?["type"] == "lelantusmint") {
|
||||
final asm = output["scriptPubKey"]?["asm"] as String?;
|
||||
if (asm != null) {
|
||||
if (asm.startsWith("OP_LELANTUSJMINT")) {
|
||||
isJMint = true;
|
||||
break;
|
||||
} else if (asm.startsWith("OP_LELANTUSMINT")) {
|
||||
isMint = true;
|
||||
break;
|
||||
} else {
|
||||
Logging.instance.log(
|
||||
"Unknown mint op code found for lelantusmint tx: ${txObject["txid"]}",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Logging.instance.log(
|
||||
"ASM for lelantusmint tx: ${txObject["txid"]} is null!",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isar_models.TransactionType type;
|
||||
isar_models.TransactionSubType subType =
|
||||
isar_models.TransactionSubType.none;
|
||||
int amount;
|
||||
if (foundInSenders) {
|
||||
type = isar_models.TransactionType.outgoing;
|
||||
amount = inputAmtSentFromWallet;
|
||||
Set<String> inputAddresses = {};
|
||||
Set<String> outputAddresses = {};
|
||||
|
||||
if (txObject["vout"][0]["scriptPubKey"]["type"] == "lelantusmint") {
|
||||
subType = isar_models.TransactionSubType.mint;
|
||||
}
|
||||
} else {
|
||||
type = isar_models.TransactionType.incoming;
|
||||
amount = outputAmtAddressedToWallet;
|
||||
}
|
||||
|
||||
final transactionAddress =
|
||||
allAddresses.firstWhere((e) => e.value == outAddress,
|
||||
orElse: () => isar_models.Address(
|
||||
walletId: walletId,
|
||||
value: outAddress,
|
||||
derivationIndex: -1,
|
||||
derivationPath: null,
|
||||
type: isar_models.AddressType.nonWallet,
|
||||
subType: isar_models.AddressSubType.nonWallet,
|
||||
publicKey: [],
|
||||
));
|
||||
|
||||
List<isar_models.Output> outs = [];
|
||||
List<isar_models.Input> ins = [];
|
||||
|
||||
for (final json in txObject["vin"] as List) {
|
||||
bool isCoinBase = json['coinbase'] != null;
|
||||
final input = isar_models.Input(
|
||||
txid: json['txid'] as String? ?? "",
|
||||
vout: json['vout'] as int? ?? -1,
|
||||
scriptSig: json['scriptSig']?['hex'] as String?,
|
||||
scriptSigAsm: json['scriptSig']?['asm'] as String?,
|
||||
isCoinbase: isCoinBase ? isCoinBase : json['is_coinbase'] as bool?,
|
||||
sequence: json['sequence'] as int?,
|
||||
innerRedeemScriptAsm: json['innerRedeemscriptAsm'] as String?,
|
||||
);
|
||||
ins.add(input);
|
||||
}
|
||||
|
||||
for (final json in txObject["vout"] as List) {
|
||||
final output = isar_models.Output(
|
||||
scriptPubKey: json['scriptPubKey']?['hex'] as String?,
|
||||
scriptPubKeyAsm: json['scriptPubKey']?['asm'] as String?,
|
||||
scriptPubKeyType: json['scriptPubKey']?['type'] as String?,
|
||||
scriptPubKeyAddress:
|
||||
json["scriptPubKey"]?["addresses"]?[0] as String? ??
|
||||
json['scriptPubKey']['type'] as String,
|
||||
value: Amount.fromDecimal(
|
||||
Decimal.parse(json["value"].toString()),
|
||||
fractionDigits: coin.decimals,
|
||||
).raw.toInt(),
|
||||
);
|
||||
outs.add(output);
|
||||
}
|
||||
|
||||
final tx = isar_models.Transaction(
|
||||
walletId: walletId,
|
||||
txid: txObject["txid"] as String,
|
||||
timestamp: txObject["blocktime"] as int? ??
|
||||
(DateTime.now().millisecondsSinceEpoch ~/ 1000),
|
||||
type: type,
|
||||
subType: subType,
|
||||
amount: amount,
|
||||
amountString: Amount(
|
||||
rawValue: BigInt.from(amount),
|
||||
fractionDigits: Coin.firo.decimals,
|
||||
).toJsonString(),
|
||||
fee: fees,
|
||||
height: txObject["height"] as int? ?? 0,
|
||||
isCancelled: false,
|
||||
isLelantus: false,
|
||||
slateId: null,
|
||||
otherData: null,
|
||||
nonce: null,
|
||||
inputs: ins,
|
||||
outputs: outs,
|
||||
Amount totalInputValue = Amount(
|
||||
rawValue: BigInt.zero,
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
Amount totalOutputValue = Amount(
|
||||
rawValue: BigInt.zero,
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
|
||||
txnsData.add(Tuple2(tx, transactionAddress));
|
||||
Amount amountSentFromWallet = Amount(
|
||||
rawValue: BigInt.zero,
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
Amount amountReceivedInWallet = Amount(
|
||||
rawValue: BigInt.zero,
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
Amount changeAmount = Amount(
|
||||
rawValue: BigInt.zero,
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
|
||||
// Parse mint transaction ================================================
|
||||
// We should be able to assume this belongs to this wallet
|
||||
if (isMint) {
|
||||
List<isar_models.Input> ins = [];
|
||||
|
||||
// Parse inputs
|
||||
for (final input in inputList) {
|
||||
// Both value and address should not be null for a mint
|
||||
final address = input["address"] as String?;
|
||||
final value = input["valueSat"] as int?;
|
||||
|
||||
// We should not need to check whether the mint belongs to this
|
||||
// wallet as any tx we look up will be looked up by one of this
|
||||
// wallet's addresses
|
||||
if (address != null && value != null) {
|
||||
totalInputValue += value.toAmountAsRaw(
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
}
|
||||
|
||||
ins.add(
|
||||
isar_models.Input(
|
||||
txid: input['txid'] as String? ?? "",
|
||||
vout: input['vout'] as int? ?? -1,
|
||||
scriptSig: input['scriptSig']?['hex'] as String?,
|
||||
scriptSigAsm: input['scriptSig']?['asm'] as String?,
|
||||
isCoinbase: input['is_coinbase'] as bool?,
|
||||
sequence: input['sequence'] as int?,
|
||||
innerRedeemScriptAsm: input['innerRedeemscriptAsm'] as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Parse outputs
|
||||
for (final output in outputList) {
|
||||
// get value
|
||||
final value = Amount.fromDecimal(
|
||||
Decimal.parse(output["value"].toString()),
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
|
||||
// add value to total
|
||||
totalOutputValue += value;
|
||||
}
|
||||
|
||||
final fee = totalInputValue - totalOutputValue;
|
||||
final tx = isar_models.Transaction(
|
||||
walletId: walletId,
|
||||
txid: txObject["txid"] as String,
|
||||
timestamp: txObject["blocktime"] as int? ??
|
||||
(DateTime.now().millisecondsSinceEpoch ~/ 1000),
|
||||
type: isar_models.TransactionType.sentToSelf,
|
||||
subType: isar_models.TransactionSubType.mint,
|
||||
amount: totalOutputValue.raw.toInt(),
|
||||
amountString: totalOutputValue.toJsonString(),
|
||||
fee: fee.raw.toInt(),
|
||||
height: txObject["height"] as int?,
|
||||
isCancelled: false,
|
||||
isLelantus: true,
|
||||
slateId: null,
|
||||
otherData: null,
|
||||
nonce: null,
|
||||
inputs: ins,
|
||||
outputs: [],
|
||||
);
|
||||
|
||||
txnsData.add(Tuple2(tx, null));
|
||||
|
||||
// Otherwise parse JMint transaction ===================================
|
||||
} else if (isJMint) {
|
||||
Amount jMintFees = Amount(
|
||||
rawValue: BigInt.zero,
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
|
||||
// Parse inputs
|
||||
List<isar_models.Input> ins = [];
|
||||
for (final input in inputList) {
|
||||
// JMint fee
|
||||
final nFee = Decimal.tryParse(input["nFees"].toString());
|
||||
if (nFee != null) {
|
||||
final fees = Amount.fromDecimal(
|
||||
nFee,
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
|
||||
jMintFees += fees;
|
||||
}
|
||||
|
||||
ins.add(
|
||||
isar_models.Input(
|
||||
txid: input['txid'] as String? ?? "",
|
||||
vout: input['vout'] as int? ?? -1,
|
||||
scriptSig: input['scriptSig']?['hex'] as String?,
|
||||
scriptSigAsm: input['scriptSig']?['asm'] as String?,
|
||||
isCoinbase: input['is_coinbase'] as bool?,
|
||||
sequence: input['sequence'] as int?,
|
||||
innerRedeemScriptAsm: input['innerRedeemscriptAsm'] as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool nonWalletAddressFoundInOutputs = false;
|
||||
|
||||
// Parse outputs
|
||||
List<isar_models.Output> outs = [];
|
||||
for (final output in outputList) {
|
||||
// get value
|
||||
final value = Amount.fromDecimal(
|
||||
Decimal.parse(output["value"].toString()),
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
|
||||
// add value to total
|
||||
totalOutputValue += value;
|
||||
|
||||
final address = output["scriptPubKey"]?["addresses"]?[0] as String? ??
|
||||
output['scriptPubKey']?['address'] as String?;
|
||||
|
||||
if (address != null) {
|
||||
outputAddresses.add(address);
|
||||
if (receivingAddresses.contains(address) ||
|
||||
changeAddresses.contains(address)) {
|
||||
amountReceivedInWallet += value;
|
||||
} else {
|
||||
nonWalletAddressFoundInOutputs = true;
|
||||
}
|
||||
}
|
||||
|
||||
outs.add(
|
||||
isar_models.Output(
|
||||
scriptPubKey: output['scriptPubKey']?['hex'] as String?,
|
||||
scriptPubKeyAsm: output['scriptPubKey']?['asm'] as String?,
|
||||
scriptPubKeyType: output['scriptPubKey']?['type'] as String?,
|
||||
scriptPubKeyAddress: address ?? "jmint",
|
||||
value: value.raw.toInt(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const subType = isar_models.TransactionSubType.join;
|
||||
final type = nonWalletAddressFoundInOutputs
|
||||
? isar_models.TransactionType.outgoing
|
||||
: isar_models.TransactionType.incoming;
|
||||
|
||||
final amount = nonWalletAddressFoundInOutputs
|
||||
? totalOutputValue
|
||||
: amountReceivedInWallet;
|
||||
|
||||
final possibleNonWalletAddresses =
|
||||
receivingAddresses.difference(outputAddresses);
|
||||
final possibleReceivingAddresses =
|
||||
receivingAddresses.intersection(outputAddresses);
|
||||
|
||||
final transactionAddress = nonWalletAddressFoundInOutputs
|
||||
? isar_models.Address(
|
||||
walletId: walletId,
|
||||
value: possibleNonWalletAddresses.first,
|
||||
derivationIndex: -1,
|
||||
derivationPath: null,
|
||||
type: isar_models.AddressType.nonWallet,
|
||||
subType: isar_models.AddressSubType.nonWallet,
|
||||
publicKey: [],
|
||||
)
|
||||
: allAddresses.firstWhere(
|
||||
(e) => e.value == possibleReceivingAddresses.first,
|
||||
);
|
||||
|
||||
final tx = isar_models.Transaction(
|
||||
walletId: walletId,
|
||||
txid: txObject["txid"] as String,
|
||||
timestamp: txObject["blocktime"] as int? ??
|
||||
(DateTime.now().millisecondsSinceEpoch ~/ 1000),
|
||||
type: type,
|
||||
subType: subType,
|
||||
amount: amount.raw.toInt(),
|
||||
amountString: amount.toJsonString(),
|
||||
fee: jMintFees.raw.toInt(),
|
||||
height: txObject["height"] as int?,
|
||||
isCancelled: false,
|
||||
isLelantus: true,
|
||||
slateId: null,
|
||||
otherData: null,
|
||||
nonce: null,
|
||||
inputs: ins,
|
||||
outputs: outs,
|
||||
);
|
||||
|
||||
txnsData.add(Tuple2(tx, transactionAddress));
|
||||
|
||||
// Assume non lelantus transaction =====================================
|
||||
} else {
|
||||
// parse inputs
|
||||
List<isar_models.Input> ins = [];
|
||||
for (final input in inputList) {
|
||||
final valueSat = input["valueSat"] as int?;
|
||||
final address = input["address"] as String? ??
|
||||
input["scriptPubKey"]?["address"] as String? ??
|
||||
input["scriptPubKey"]?["addresses"]?[0] as String?;
|
||||
|
||||
if (address != null && valueSat != null) {
|
||||
final value = valueSat.toAmountAsRaw(
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
|
||||
// add value to total
|
||||
totalInputValue += value;
|
||||
inputAddresses.add(address);
|
||||
|
||||
// if input was from my wallet, add value to amount sent
|
||||
if (receivingAddresses.contains(address) ||
|
||||
changeAddresses.contains(address)) {
|
||||
amountSentFromWallet += value;
|
||||
}
|
||||
}
|
||||
|
||||
ins.add(
|
||||
isar_models.Input(
|
||||
txid: input['txid'] as String,
|
||||
vout: input['vout'] as int? ?? -1,
|
||||
scriptSig: input['scriptSig']?['hex'] as String?,
|
||||
scriptSigAsm: input['scriptSig']?['asm'] as String?,
|
||||
isCoinbase: input['is_coinbase'] as bool?,
|
||||
sequence: input['sequence'] as int?,
|
||||
innerRedeemScriptAsm: input['innerRedeemscriptAsm'] as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// parse outputs
|
||||
List<isar_models.Output> outs = [];
|
||||
for (final output in outputList) {
|
||||
// get value
|
||||
final value = Amount.fromDecimal(
|
||||
Decimal.parse(output["value"].toString()),
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
|
||||
// add value to total
|
||||
totalOutputValue += value;
|
||||
|
||||
// get output address
|
||||
final address = output["scriptPubKey"]?["addresses"]?[0] as String? ??
|
||||
output["scriptPubKey"]?["address"] as String?;
|
||||
if (address != null) {
|
||||
outputAddresses.add(address);
|
||||
|
||||
// if output was to my wallet, add value to amount received
|
||||
if (receivingAddresses.contains(address)) {
|
||||
amountReceivedInWallet += value;
|
||||
} else if (changeAddresses.contains(address)) {
|
||||
changeAmount += value;
|
||||
}
|
||||
}
|
||||
|
||||
outs.add(
|
||||
isar_models.Output(
|
||||
scriptPubKey: output['scriptPubKey']?['hex'] as String?,
|
||||
scriptPubKeyAsm: output['scriptPubKey']?['asm'] as String?,
|
||||
scriptPubKeyType: output['scriptPubKey']?['type'] as String?,
|
||||
scriptPubKeyAddress: address ?? "",
|
||||
value: value.raw.toInt(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final mySentFromAddresses = [
|
||||
...receivingAddresses.intersection(inputAddresses),
|
||||
...changeAddresses.intersection(inputAddresses)
|
||||
];
|
||||
final myReceivedOnAddresses =
|
||||
receivingAddresses.intersection(outputAddresses);
|
||||
final myChangeReceivedOnAddresses =
|
||||
changeAddresses.intersection(outputAddresses);
|
||||
|
||||
final fee = totalInputValue - totalOutputValue;
|
||||
|
||||
// this is the address initially used to fetch the txid
|
||||
isar_models.Address transactionAddress =
|
||||
txObject["address"] as isar_models.Address;
|
||||
|
||||
isar_models.TransactionType type;
|
||||
Amount amount;
|
||||
if (mySentFromAddresses.isNotEmpty &&
|
||||
myReceivedOnAddresses.isNotEmpty) {
|
||||
// tx is sent to self
|
||||
type = isar_models.TransactionType.sentToSelf;
|
||||
|
||||
// should be 0
|
||||
amount = amountSentFromWallet -
|
||||
amountReceivedInWallet -
|
||||
fee -
|
||||
changeAmount;
|
||||
} else if (mySentFromAddresses.isNotEmpty) {
|
||||
// outgoing tx
|
||||
type = isar_models.TransactionType.outgoing;
|
||||
amount = amountSentFromWallet - changeAmount - fee;
|
||||
|
||||
final possible =
|
||||
outputAddresses.difference(myChangeReceivedOnAddresses).first;
|
||||
|
||||
if (transactionAddress.value != possible) {
|
||||
transactionAddress = isar_models.Address(
|
||||
walletId: walletId,
|
||||
value: possible,
|
||||
derivationIndex: -1,
|
||||
derivationPath: null,
|
||||
subType: isar_models.AddressSubType.nonWallet,
|
||||
type: isar_models.AddressType.nonWallet,
|
||||
publicKey: [],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// incoming tx
|
||||
type = isar_models.TransactionType.incoming;
|
||||
amount = amountReceivedInWallet;
|
||||
}
|
||||
|
||||
final tx = isar_models.Transaction(
|
||||
walletId: walletId,
|
||||
txid: txObject["txid"] as String,
|
||||
timestamp: txObject["blocktime"] as int? ??
|
||||
(DateTime.now().millisecondsSinceEpoch ~/ 1000),
|
||||
type: type,
|
||||
subType: isar_models.TransactionSubType.none,
|
||||
// amount may overflow. Deprecated. Use amountString
|
||||
amount: amount.raw.toInt(),
|
||||
amountString: amount.toJsonString(),
|
||||
fee: fee.raw.toInt(),
|
||||
height: txObject["height"] as int?,
|
||||
isCancelled: false,
|
||||
isLelantus: false,
|
||||
slateId: null,
|
||||
otherData: null,
|
||||
nonce: null,
|
||||
inputs: ins,
|
||||
outputs: outs,
|
||||
);
|
||||
|
||||
txnsData.add(Tuple2(tx, transactionAddress));
|
||||
}
|
||||
}
|
||||
|
||||
await db.addNewTransactionData(txnsData, walletId);
|
||||
|
@ -4570,8 +4703,6 @@ class FiroWallet extends CoinServiceAPI
|
|||
final response = await cachedElectrumXClient.getUsedCoinSerials(
|
||||
coin: coin,
|
||||
);
|
||||
print("getUsedCoinSerials");
|
||||
print(response);
|
||||
return response;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("Exception rethrown in firo_wallet.dart: $e\n$s",
|
||||
|
|
|
@ -35,9 +35,10 @@ mixin FiroHive {
|
|||
}
|
||||
|
||||
// mintIndex
|
||||
int? firoGetMintIndex() {
|
||||
int firoGetMintIndex() {
|
||||
return DB.instance.get<dynamic>(boxName: _walletId, key: "mintIndex")
|
||||
as int?;
|
||||
as int? ??
|
||||
0;
|
||||
}
|
||||
|
||||
Future<void> firoUpdateMintIndex(int mintIndex) async {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
|
@ -50,4 +52,15 @@ abstract class Util {
|
|||
}
|
||||
return MaterialColor(color.value, swatch);
|
||||
}
|
||||
|
||||
static void printJson(dynamic json) {
|
||||
if (json is Map || json is List) {
|
||||
final spaces = ' ' * 4;
|
||||
final encoder = JsonEncoder.withIndent(spaces);
|
||||
final pretty = encoder.convert(json);
|
||||
log(pretty);
|
||||
} else {
|
||||
log(dynamic.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue