2023-05-26 21:21:16 +00:00
/ *
* This file is part of Stack Wallet .
*
* Copyright ( c ) 2023 Cypher Stack
* All Rights Reserved .
* The code is distributed under GPLv3 license , see LICENSE file for details .
* Generated by Cypher Stack on 2023 - 05 - 26
*
* /
2022-08-26 08:11:35 +00:00
import ' dart:async ' ;
import ' dart:convert ' ;
import ' dart:io ' ;
import ' dart:isolate ' ;
import ' dart:math ' ;
2023-05-16 17:05:25 +00:00
import ' package:bip32/bip32.dart ' as bip32 ;
2022-08-26 08:11:35 +00:00
import ' package:bip39/bip39.dart ' as bip39 ;
import ' package:bitcoindart/bitcoindart.dart ' ;
import ' package:decimal/decimal.dart ' ;
import ' package:flutter/foundation.dart ' ;
2023-01-12 00:16:17 +00:00
import ' package:isar/isar.dart ' ;
2022-08-26 08:11:35 +00:00
import ' package:lelantus/lelantus.dart ' ;
2023-03-01 21:52:13 +00:00
import ' package:stackwallet/db/isar/main_db.dart ' ;
2022-08-26 08:11:35 +00:00
import ' package:stackwallet/electrumx_rpc/cached_electrumx.dart ' ;
import ' package:stackwallet/electrumx_rpc/electrumx.dart ' ;
2023-01-12 00:16:17 +00:00
import ' package:stackwallet/models/balance.dart ' ;
import ' package:stackwallet/models/isar/models/isar_models.dart ' as isar_models ;
2022-08-26 08:11:35 +00:00
import ' package:stackwallet/models/lelantus_fee_data.dart ' ;
import ' package:stackwallet/models/paymint/fee_object_model.dart ' ;
2023-03-09 19:49:39 +00:00
import ' package:stackwallet/models/signing_data.dart ' ;
2022-08-26 08:11:35 +00:00
import ' package:stackwallet/services/coins/coin_service.dart ' ;
import ' package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart ' ;
import ' package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart ' ;
import ' package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart ' ;
import ' package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart ' ;
import ' package:stackwallet/services/event_bus/global_event_bus.dart ' ;
2023-01-12 18:46:01 +00:00
import ' package:stackwallet/services/mixins/wallet_cache.dart ' ;
import ' package:stackwallet/services/mixins/wallet_db.dart ' ;
2023-04-08 00:44:43 +00:00
import ' package:stackwallet/services/mixins/xpubable.dart ' ;
2022-08-26 08:11:35 +00:00
import ' package:stackwallet/services/node_service.dart ' ;
import ' package:stackwallet/services/transaction_notification_tracker.dart ' ;
import ' package:stackwallet/utilities/address_utils.dart ' ;
2023-04-06 21:24:56 +00:00
import ' package:stackwallet/utilities/amount/amount.dart ' ;
2023-02-03 22:34:06 +00:00
import ' package:stackwallet/utilities/bip32_utils.dart ' ;
2022-08-26 08:11:35 +00:00
import ' package:stackwallet/utilities/constants.dart ' ;
import ' package:stackwallet/utilities/default_nodes.dart ' ;
import ' package:stackwallet/utilities/enums/coin_enum.dart ' ;
2023-03-09 19:49:39 +00:00
import ' package:stackwallet/utilities/enums/derive_path_type_enum.dart ' ;
2022-09-07 15:56:10 +00:00
import ' package:stackwallet/utilities/enums/fee_rate_type_enum.dart ' ;
2022-08-26 08:11:35 +00:00
import ' package:stackwallet/utilities/flutter_secure_storage_interface.dart ' ;
import ' package:stackwallet/utilities/format.dart ' ;
import ' package:stackwallet/utilities/logger.dart ' ;
import ' package:stackwallet/utilities/prefs.dart ' ;
2023-05-09 17:54:15 +00:00
import ' package:stackwallet/widgets/crypto_notifications.dart ' ;
2022-08-26 08:11:35 +00:00
import ' package:tuple/tuple.dart ' ;
2022-09-09 15:07:27 +00:00
import ' package:uuid/uuid.dart ' ;
2022-08-26 08:11:35 +00:00
2022-09-07 15:56:10 +00:00
const DUST_LIMIT = 1000 ;
2022-08-26 08:11:35 +00:00
const MINIMUM_CONFIRMATIONS = 1 ;
2023-07-25 15:03:26 +00:00
const MINT_LIMIT = 5001 * 100000000 ;
const MINT_LIMIT_TESTNET = 1001 * 100000000 ;
2022-08-26 08:11:35 +00:00
const JMINT_INDEX = 5 ;
const MINT_INDEX = 2 ;
const TRANSACTION_LELANTUS = 8 ;
const ANONYMITY_SET_EMPTY_ID = 0 ;
const String GENESIS_HASH_MAINNET =
" 4381deb85b1b2c9843c222944b616d997516dcbd6a964e1eaf0def0830695233 " ;
const String GENESIS_HASH_TESTNET =
" aa22adcc12becaf436027ffe62a8fb21b234c58c23865291e5dc52cf53f64fca " ;
final firoNetwork = NetworkType (
messagePrefix: ' \x18 Zcoin Signed Message: \n ' ,
bech32: ' bc ' ,
bip32: Bip32Type ( public: 0x0488b21e , private: 0x0488ade4 ) ,
pubKeyHash: 0x52 ,
scriptHash: 0x07 ,
wif: 0xd2 ) ;
final firoTestNetwork = NetworkType (
messagePrefix: ' \x18 Zcoin Signed Message: \n ' ,
bech32: ' bc ' ,
bip32: Bip32Type ( public: 0x043587cf , private: 0x04358394 ) ,
pubKeyHash: 0x41 ,
scriptHash: 0xb2 ,
wif: 0xb9 ) ;
// isolate
Map < ReceivePort , Isolate > isolates = { } ;
Future < ReceivePort > getIsolate ( Map < String , dynamic > arguments ) async {
ReceivePort receivePort =
ReceivePort ( ) ; //port for isolate to receive messages.
arguments [ ' sendPort ' ] = receivePort . sendPort ;
Logging . instance
. log ( " starting isolate ${ arguments [ ' function ' ] } " , level: LogLevel . Info ) ;
Isolate isolate = await Isolate . spawn ( executeNative , arguments ) ;
Logging . instance . log ( " isolate spawned! " , level: LogLevel . Info ) ;
isolates [ receivePort ] = isolate ;
return receivePort ;
}
Future < void > executeNative ( Map < String , dynamic > arguments ) async {
await Logging . instance . initInIsolate ( ) ;
final sendPort = arguments [ ' sendPort ' ] as SendPort ;
final function = arguments [ ' function ' ] as String ;
try {
if ( function = = " createJoinSplit " ) {
final spendAmount = arguments [ ' spendAmount ' ] as int ;
final address = arguments [ ' address ' ] as String ;
final subtractFeeFromAmount = arguments [ ' subtractFeeFromAmount ' ] as bool ;
final mnemonic = arguments [ ' mnemonic ' ] as String ;
2023-02-03 22:34:06 +00:00
final mnemonicPassphrase = arguments [ ' mnemonicPassphrase ' ] as String ;
2022-08-26 08:11:35 +00:00
final index = arguments [ ' index ' ] as int ;
final lelantusEntries =
arguments [ ' lelantusEntries ' ] as List < DartLelantusEntry > ;
final coin = arguments [ ' coin ' ] as Coin ;
final network = arguments [ ' network ' ] as NetworkType ? ;
final locktime = arguments [ ' locktime ' ] as int ;
final anonymitySets = arguments [ ' _anonymity_sets ' ] as List < Map > ? ;
if ( ! ( network = = null | | anonymitySets = = null ) ) {
var joinSplit = await isolateCreateJoinSplitTransaction (
2023-01-17 18:31:07 +00:00
spendAmount ,
address ,
subtractFeeFromAmount ,
mnemonic ,
2023-02-03 22:34:06 +00:00
mnemonicPassphrase ,
2023-01-17 18:31:07 +00:00
index ,
lelantusEntries ,
locktime ,
coin ,
network ,
anonymitySets ,
) ;
2022-08-26 08:11:35 +00:00
sendPort . send ( joinSplit ) ;
return ;
}
} else if ( function = = " estimateJoinSplit " ) {
final spendAmount = arguments [ ' spendAmount ' ] as int ;
final subtractFeeFromAmount = arguments [ ' subtractFeeFromAmount ' ] as bool ? ;
final lelantusEntries =
arguments [ ' lelantusEntries ' ] as List < DartLelantusEntry > ;
final coin = arguments [ ' coin ' ] as Coin ;
if ( ! ( subtractFeeFromAmount = = null ) ) {
var feeData = await isolateEstimateJoinSplitFee (
spendAmount , subtractFeeFromAmount , lelantusEntries , coin ) ;
sendPort . send ( feeData ) ;
return ;
}
} else if ( function = = " restore " ) {
final latestSetId = arguments [ ' latestSetId ' ] as int ;
final setDataMap = arguments [ ' setDataMap ' ] as Map ;
2023-05-16 19:29:48 +00:00
final usedSerialNumbers = arguments [ ' usedSerialNumbers ' ] as List < String > ;
2022-08-26 08:11:35 +00:00
final mnemonic = arguments [ ' mnemonic ' ] as String ;
2023-02-03 22:34:06 +00:00
final mnemonicPassphrase = arguments [ ' mnemonicPassphrase ' ] as String ;
2022-08-26 08:11:35 +00:00
final coin = arguments [ ' coin ' ] as Coin ;
2023-05-16 19:29:48 +00:00
final network = arguments [ ' network ' ] as NetworkType ;
2023-07-24 18:30:01 +00:00
final walletId = arguments [ ' walletId ' ] as String ;
2023-05-16 19:29:48 +00:00
final restoreData = await isolateRestore (
mnemonic ,
mnemonicPassphrase ,
coin ,
latestSetId ,
setDataMap ,
usedSerialNumbers ,
network ,
2023-07-24 18:30:01 +00:00
walletId ,
2023-05-16 19:29:48 +00:00
) ;
sendPort . send ( restoreData ) ;
return ;
2022-08-26 08:11:35 +00:00
}
2023-05-16 17:05:25 +00:00
2022-08-26 08:11:35 +00:00
Logging . instance . log (
" Error Arguments for $ function not formatted correctly " ,
level: LogLevel . Fatal ) ;
sendPort . send ( " Error " ) ;
} catch ( e , s ) {
Logging . instance . log (
" An error was thrown in this isolate $ function : $ e \n $ s " ,
level: LogLevel . Error ) ;
sendPort . send ( " Error " ) ;
} finally {
2022-09-06 15:34:39 +00:00
await Logging . instance . isar ? . close ( ) ;
2022-08-26 08:11:35 +00:00
}
}
void stop ( ReceivePort port ) {
Isolate ? isolate = isolates . remove ( port ) ;
if ( isolate ! = null ) {
Logging . instance . log ( ' Stopping Isolate... ' , level: LogLevel . Info ) ;
isolate . kill ( priority: Isolate . immediate ) ;
isolate = null ;
}
}
Future < Map < String , dynamic > > isolateRestore (
String mnemonic ,
2023-02-03 22:34:06 +00:00
String mnemonicPassphrase ,
2022-08-26 08:11:35 +00:00
Coin coin ,
int _latestSetId ,
Map < dynamic , dynamic > _setDataMap ,
2023-05-16 19:29:48 +00:00
List < String > _usedSerialNumbers ,
2022-08-26 08:11:35 +00:00
NetworkType network ,
2023-07-24 18:30:01 +00:00
String walletId ,
2022-08-26 08:11:35 +00:00
) async {
List < int > jindexes = [ ] ;
2023-07-24 18:30:01 +00:00
List < isar_models . LelantusCoin > lelantusCoins = [ ] ;
2022-08-26 08:11:35 +00:00
final List < String > spendTxIds = [ ] ;
2023-07-24 18:30:01 +00:00
int lastFoundIndex = 0 ;
int currentIndex = 0 ;
2022-08-26 08:11:35 +00:00
try {
2023-05-16 19:29:48 +00:00
Set < String > usedSerialNumbersSet = _usedSerialNumbers . toSet ( ) ;
2022-08-26 08:11:35 +00:00
2023-02-03 22:34:06 +00:00
final root = await Bip32Utils . getBip32Root (
mnemonic ,
mnemonicPassphrase ,
network ,
) ;
2022-10-18 17:28:14 +00:00
while ( currentIndex < lastFoundIndex + 50 ) {
2023-02-03 22:34:06 +00:00
final _derivePath = constructDerivePath (
networkWIF: network . wif ,
chain: MINT_INDEX ,
index: currentIndex ,
) ;
2023-05-16 19:29:48 +00:00
final bip32 . BIP32 mintKeyPair = await Bip32Utils . getBip32NodeFromRoot (
root ,
_derivePath ,
) ;
final String mintTag = CreateTag (
2023-05-16 20:08:25 +00:00
Format . uint8listToString ( mintKeyPair . privateKey ! ) ,
currentIndex ,
Format . uint8listToString ( mintKeyPair . identifier ) ,
isTestnet: coin = = Coin . firoTestNet ,
) ;
2022-08-26 08:11:35 +00:00
2023-07-24 18:30:01 +00:00
for ( int setId = 1 ; setId < = _latestSetId ; setId + + ) {
2023-05-16 20:08:25 +00:00
final setData = _setDataMap [ setId ] as Map ;
final foundCoin = ( setData [ " coins " ] as List ) . firstWhere (
( e ) = > e [ 1 ] = = mintTag ,
orElse: ( ) = > < Object > [ ] ,
) ;
2022-08-26 08:11:35 +00:00
if ( foundCoin . length = = 4 ) {
lastFoundIndex = currentIndex ;
2023-05-16 20:08:25 +00:00
final String publicCoin = foundCoin [ 0 ] as String ;
final String txId = foundCoin [ 3 ] as String ;
// this value will either be an int or a String
final dynamic thirdValue = foundCoin [ 2 ] ;
if ( thirdValue is int ) {
final int amount = thirdValue ;
2023-05-16 19:29:48 +00:00
final String serialNumber = GetSerialNumber (
amount ,
Format . uint8listToString ( mintKeyPair . privateKey ! ) ,
currentIndex ,
isTestnet: coin = = Coin . firoTestNet ,
) ;
final bool isUsed = usedSerialNumbersSet . contains ( serialNumber ) ;
2023-07-24 18:30:01 +00:00
lelantusCoins . removeWhere ( ( e ) = >
e . txid = = txId & &
2023-07-24 21:42:45 +00:00
e . mintIndex = = currentIndex & &
2023-07-24 18:30:01 +00:00
e . anonymitySetId ! = setId ) ;
lelantusCoins . add (
isar_models . LelantusCoin (
walletId: walletId ,
2023-07-24 21:42:45 +00:00
mintIndex: currentIndex ,
2023-07-24 18:30:01 +00:00
value: amount . toString ( ) ,
publicCoin: publicCoin ,
txid: txId ,
anonymitySetId: setId ,
isUsed: isUsed ,
2023-07-24 21:42:45 +00:00
isJMint: false ,
otherData: null ,
2023-07-24 18:30:01 +00:00
) ,
2023-05-16 20:08:25 +00:00
) ;
2023-05-16 20:13:14 +00:00
Logging . instance . log (
" amount $ amount used $ isUsed " ,
level: LogLevel . Info ,
) ;
2023-05-16 20:08:25 +00:00
} else if ( thirdValue is String ) {
2023-05-16 20:13:14 +00:00
final int keyPath = GetAesKeyPath ( publicCoin ) ;
final String derivePath = constructDerivePath (
2023-02-03 22:34:06 +00:00
networkWIF: network . wif ,
chain: JMINT_INDEX ,
index: keyPath ,
) ;
2023-05-16 20:08:25 +00:00
final aesKeyPair = await Bip32Utils . getBip32NodeFromRoot (
root ,
derivePath ,
) ;
2023-02-03 22:34:06 +00:00
2022-08-26 08:11:35 +00:00
if ( aesKeyPair . privateKey ! = null ) {
2023-05-16 20:13:14 +00:00
final String aesPrivateKey = Format . uint8listToString (
2023-05-16 20:08:25 +00:00
aesKeyPair . privateKey ! ,
) ;
2023-05-16 20:13:14 +00:00
final int amount = decryptMintAmount (
2022-08-26 08:11:35 +00:00
aesPrivateKey ,
2023-05-16 20:08:25 +00:00
thirdValue ,
2022-08-26 08:11:35 +00:00
) ;
2023-05-16 20:13:14 +00:00
final String serialNumber = GetSerialNumber (
amount ,
Format . uint8listToString ( mintKeyPair . privateKey ! ) ,
currentIndex ,
isTestnet: coin = = Coin . firoTestNet ,
) ;
2022-08-26 08:11:35 +00:00
bool isUsed = usedSerialNumbersSet . contains ( serialNumber ) ;
2023-07-24 18:30:01 +00:00
lelantusCoins . removeWhere ( ( e ) = >
e . txid = = txId & &
2023-07-24 21:42:45 +00:00
e . mintIndex = = currentIndex & &
2023-07-24 18:30:01 +00:00
e . anonymitySetId ! = setId ) ;
lelantusCoins . add (
isar_models . LelantusCoin (
walletId: walletId ,
2023-07-24 21:42:45 +00:00
mintIndex: currentIndex ,
2023-07-24 18:30:01 +00:00
value: amount . toString ( ) ,
publicCoin: publicCoin ,
txid: txId ,
anonymitySetId: setId ,
isUsed: isUsed ,
2023-07-24 21:42:45 +00:00
isJMint: true ,
otherData: null ,
2023-07-24 18:30:01 +00:00
) ,
2023-05-16 20:13:14 +00:00
) ;
2022-08-26 08:11:35 +00:00
jindexes . add ( currentIndex ) ;
2023-05-16 20:08:25 +00:00
spendTxIds . add ( txId ) ;
2023-05-16 20:20:18 +00:00
} else {
Logging . instance . log (
" AES keypair derivation issue for derive path: $ derivePath " ,
level: LogLevel . Warning ,
) ;
2022-08-26 08:11:35 +00:00
}
2023-05-16 20:17:12 +00:00
} else {
Logging . instance . log (
" Unexpected coin found: $ foundCoin " ,
level: LogLevel . Warning ,
) ;
2022-08-26 08:11:35 +00:00
}
2023-05-16 20:17:12 +00:00
} else {
Logging . instance . log (
" Coin not found in data with the mint tag: $ mintTag " ,
level: LogLevel . Warning ,
) ;
2022-08-26 08:11:35 +00:00
}
}
currentIndex + + ;
}
} catch ( e , s ) {
Logging . instance . log ( " Exception rethrown from isolateRestore(): $ e \n $ s " ,
level: LogLevel . Info ) ;
rethrow ;
}
Map < String , dynamic > result = { } ;
// Logging.instance.log("mints $lelantusCoins", addToDebugMessagesDB: false);
// Logging.instance.log("jmints $spendTxIds", addToDebugMessagesDB: false);
result [ ' _lelantus_coins ' ] = lelantusCoins ;
result [ ' mintIndex ' ] = lastFoundIndex + 1 ;
result [ ' jindex ' ] = jindexes ;
2022-09-09 15:07:27 +00:00
result [ ' spendTxIds ' ] = spendTxIds ;
return result ;
}
Future < Map < dynamic , dynamic > > staticProcessRestore (
2023-01-12 00:16:17 +00:00
List < isar_models . Transaction > txns ,
2022-09-09 15:07:27 +00:00
Map < dynamic , dynamic > result ,
2023-01-12 00:16:17 +00:00
int currentHeight ,
2022-09-09 15:07:27 +00:00
) async {
2023-07-24 18:30:01 +00:00
List < isar_models . LelantusCoin > lelantusCoins =
result [ ' _lelantus_coins ' ] as List < isar_models . LelantusCoin > ;
2022-08-26 08:11:35 +00:00
// Edit the receive transactions with the mint fees.
2023-07-24 18:30:01 +00:00
List < isar_models . Transaction > editedTransactions = [ ] ;
for ( final coin in lelantusCoins ) {
String txid = coin . txid ;
isar_models . Transaction ? tx ;
try {
tx = txns . firstWhere ( ( e ) = > e . txid = = txid ) ;
} catch ( _ ) {
tx = null ;
}
if ( tx = = null | | tx . subType = = isar_models . TransactionSubType . join ) {
// This is a jmint.
continue ;
}
List < isar_models . Transaction > inputTxns = [ ] ;
for ( final input in tx . inputs ) {
isar_models . Transaction ? inputTx ;
2023-01-12 00:16:17 +00:00
try {
2023-07-24 18:30:01 +00:00
inputTx = txns . firstWhere ( ( e ) = > e . txid = = input . txid ) ;
2023-01-12 00:16:17 +00:00
} catch ( _ ) {
2023-07-24 18:30:01 +00:00
inputTx = null ;
2022-08-26 08:11:35 +00:00
}
2023-07-24 18:30:01 +00:00
if ( inputTx ! = null ) {
inputTxns . add ( inputTx ) ;
2022-08-26 08:11:35 +00:00
}
2023-07-24 18:30:01 +00:00
}
if ( inputTxns . isEmpty ) {
//some error.
Logging . instance . log (
" cryptic \" //some error \" occurred in staticProcessRestore on lelantus coin: $ coin " ,
level: LogLevel . Error ,
) ;
continue ;
}
2022-08-26 08:11:35 +00:00
2023-07-24 18:30:01 +00:00
int mintFee = tx . fee ;
int sharedFee = mintFee ~ / inputTxns . length ;
for ( final inputTx in inputTxns ) {
final edited = isar_models . Transaction (
walletId: inputTx . walletId ,
txid: inputTx . txid ,
timestamp: inputTx . timestamp ,
type: inputTx . type ,
subType: isar_models . TransactionSubType . mint ,
amount: inputTx . amount ,
amountString: Amount (
rawValue: BigInt . from ( inputTx . amount ) ,
fractionDigits: Coin . firo . decimals ,
) . toJsonString ( ) ,
fee: sharedFee ,
height: inputTx . height ,
isCancelled: false ,
isLelantus: true ,
slateId: null ,
otherData: txid ,
nonce: null ,
inputs: inputTx . inputs ,
outputs: inputTx . outputs ,
numberOfMessages: null ,
) . . address . value = inputTx . address . value ;
editedTransactions . add ( edited ) ;
}
2022-09-06 15:34:39 +00:00
}
2022-08-26 08:11:35 +00:00
// Logging.instance.log(editedTransactions, addToDebugMessagesDB: false);
2023-01-12 00:16:17 +00:00
Map < String , isar_models . Transaction > transactionMap = { } ;
for ( final e in txns ) {
transactionMap [ e . txid ] = e ;
}
2022-08-26 08:11:35 +00:00
// Logging.instance.log(transactionMap, addToDebugMessagesDB: false);
2023-07-24 18:30:01 +00:00
// update with edited transactions
for ( final tx in editedTransactions ) {
transactionMap [ tx . txid ] = tx ;
}
2022-08-26 08:11:35 +00:00
transactionMap . removeWhere ( ( key , value ) = >
2023-07-24 18:30:01 +00:00
lelantusCoins . any ( ( element ) = > element . txid = = key ) | |
2023-01-12 00:16:17 +00:00
( ( value . height = = - 1 | | value . height = = null ) & &
! value . isConfirmed ( currentHeight , MINIMUM_CONFIRMATIONS ) ) ) ;
2022-08-26 08:11:35 +00:00
result [ ' newTxMap ' ] = transactionMap ;
return result ;
}
Future < LelantusFeeData > isolateEstimateJoinSplitFee (
int spendAmount ,
bool subtractFeeFromAmount ,
List < DartLelantusEntry > lelantusEntries ,
Coin coin ) async {
Logging . instance . log ( " estimateJoinsplit fee " , level: LogLevel . Info ) ;
// for (int i = 0; i < lelantusEntries.length; i++) {
// Logging.instance.log(lelantusEntries[i], addToDebugMessagesDB: false);
// }
Logging . instance
. log ( " $ spendAmount $ subtractFeeFromAmount " , level: LogLevel . Info ) ;
List < int > changeToMint = List . empty ( growable: true ) ;
List < int > spendCoinIndexes = List . empty ( growable: true ) ;
// Logging.instance.log(lelantusEntries, addToDebugMessagesDB: false);
final fee = estimateFee (
spendAmount ,
subtractFeeFromAmount ,
lelantusEntries ,
changeToMint ,
spendCoinIndexes ,
isTestnet: coin = = Coin . firoTestNet ,
) ;
final estimateFeeData =
LelantusFeeData ( changeToMint [ 0 ] , fee , spendCoinIndexes ) ;
Logging . instance . log (
" estimateFeeData ${ estimateFeeData . changeToMint } ${ estimateFeeData . fee } ${ estimateFeeData . spendCoinIndexes } " ,
level: LogLevel . Info ) ;
return estimateFeeData ;
}
Future < dynamic > isolateCreateJoinSplitTransaction (
int spendAmount ,
String address ,
bool subtractFeeFromAmount ,
String mnemonic ,
2023-02-03 22:34:06 +00:00
String mnemonicPassphrase ,
2022-08-26 08:11:35 +00:00
int index ,
List < DartLelantusEntry > lelantusEntries ,
int locktime ,
Coin coin ,
NetworkType _network ,
List < Map < dynamic , dynamic > > anonymitySetsArg ,
) async {
final estimateJoinSplitFee = await isolateEstimateJoinSplitFee (
spendAmount , subtractFeeFromAmount , lelantusEntries , coin ) ;
var changeToMint = estimateJoinSplitFee . changeToMint ;
var fee = estimateJoinSplitFee . fee ;
var spendCoinIndexes = estimateJoinSplitFee . spendCoinIndexes ;
Logging . instance
. log ( " $ changeToMint $ fee $ spendCoinIndexes " , level: LogLevel . Info ) ;
if ( spendCoinIndexes . isEmpty ) {
Logging . instance . log ( " Error, Not enough funds. " , level: LogLevel . Error ) ;
return 1 ;
}
final tx = TransactionBuilder ( network: _network ) ;
tx . setLockTime ( locktime ) ;
tx . setVersion ( 3 | ( TRANSACTION_LELANTUS < < 16 ) ) ;
tx . addInput (
' 0000000000000000000000000000000000000000000000000000000000000000 ' ,
4294967295 ,
4294967295 ,
Uint8List ( 0 ) ,
) ;
2023-02-03 22:34:06 +00:00
final derivePath = constructDerivePath (
networkWIF: _network . wif ,
chain: MINT_INDEX ,
index: index ,
) ;
final jmintKeyPair = await Bip32Utils . getBip32Node (
mnemonic ,
mnemonicPassphrase ,
_network ,
derivePath ,
) ;
2022-08-26 08:11:35 +00:00
final String jmintprivatekey =
Format . uint8listToString ( jmintKeyPair . privateKey ! ) ;
final keyPath = getMintKeyPath ( changeToMint , jmintprivatekey , index ,
isTestnet: coin = = Coin . firoTestNet ) ;
2023-02-03 22:34:06 +00:00
final _derivePath = constructDerivePath (
networkWIF: _network . wif ,
chain: JMINT_INDEX ,
index: keyPath ,
) ;
final aesKeyPair = await Bip32Utils . getBip32Node (
mnemonic ,
mnemonicPassphrase ,
_network ,
_derivePath ,
) ;
2022-08-26 08:11:35 +00:00
final aesPrivateKey = Format . uint8listToString ( aesKeyPair . privateKey ! ) ;
final jmintData = createJMintScript (
changeToMint ,
Format . uint8listToString ( jmintKeyPair . privateKey ! ) ,
index ,
Format . uint8listToString ( jmintKeyPair . identifier ) ,
aesPrivateKey ,
isTestnet: coin = = Coin . firoTestNet ,
) ;
tx . addOutput (
Format . stringToUint8List ( jmintData ) ,
0 ,
) ;
int amount = spendAmount ;
if ( subtractFeeFromAmount ) {
amount - = fee ;
}
tx . addOutput (
address ,
amount ,
) ;
final extractedTx = tx . buildIncomplete ( ) ;
extractedTx . setPayload ( Uint8List ( 0 ) ) ;
final txHash = extractedTx . getId ( ) ;
final List < int > setIds = [ ] ;
final List < List < String > > anonymitySets = [ ] ;
final List < String > anonymitySetHashes = [ ] ;
final List < String > groupBlockHashes = [ ] ;
for ( var i = 0 ; i < lelantusEntries . length ; i + + ) {
final anonymitySetId = lelantusEntries [ i ] . anonymitySetId ;
if ( ! setIds . contains ( anonymitySetId ) ) {
setIds . add ( anonymitySetId ) ;
final anonymitySet = anonymitySetsArg . firstWhere (
( element ) = > element [ " setId " ] = = anonymitySetId ,
orElse: ( ) = > < String , dynamic > { } ) ;
if ( anonymitySet . isNotEmpty ) {
anonymitySetHashes . add ( anonymitySet [ ' setHash ' ] as String ) ;
groupBlockHashes . add ( anonymitySet [ ' blockHash ' ] as String ) ;
List < String > list = [ ] ;
for ( int i = 0 ; i < ( anonymitySet [ ' coins ' ] as List ) . length ; i + + ) {
list . add ( anonymitySet [ ' coins ' ] [ i ] [ 0 ] as String ) ;
}
anonymitySets . add ( list ) ;
}
}
}
final String spendScript = createJoinSplitScript (
txHash ,
spendAmount ,
subtractFeeFromAmount ,
Format . uint8listToString ( jmintKeyPair . privateKey ! ) ,
index ,
lelantusEntries ,
setIds ,
anonymitySets ,
anonymitySetHashes ,
groupBlockHashes ,
isTestnet: coin = = Coin . firoTestNet ) ;
final finalTx = TransactionBuilder ( network: _network ) ;
finalTx . setLockTime ( locktime ) ;
finalTx . setVersion ( 3 | ( TRANSACTION_LELANTUS < < 16 ) ) ;
finalTx . addOutput (
Format . stringToUint8List ( jmintData ) ,
0 ,
) ;
finalTx . addOutput (
address ,
amount ,
) ;
final extTx = finalTx . buildIncomplete ( ) ;
extTx . addInput (
Format . stringToUint8List (
' 0000000000000000000000000000000000000000000000000000000000000000 ' ) ,
4294967295 ,
4294967295 ,
Format . stringToUint8List ( " c9 " ) ,
) ;
debugPrint ( " spendscript: $ spendScript " ) ;
extTx . setPayload ( Format . stringToUint8List ( spendScript ) ) ;
final txHex = extTx . toHex ( ) ;
final txId = extTx . getId ( ) ;
Logging . instance . log ( " txid $ txId " , level: LogLevel . Info ) ;
Logging . instance . log ( " txHex: $ txHex " , level: LogLevel . Info ) ;
2023-04-11 17:20:57 +00:00
final amountAmount = Amount (
rawValue: BigInt . from ( amount ) ,
fractionDigits: coin . decimals ,
) ;
2022-08-26 08:11:35 +00:00
return {
" txid " : txId ,
" txHex " : txHex ,
" value " : amount ,
2023-04-05 22:06:31 +00:00
" fees " : Amount (
rawValue: BigInt . from ( fee ) ,
fractionDigits: coin . decimals ,
) . decimal . toDouble ( ) ,
2022-08-26 08:11:35 +00:00
" fee " : fee ,
2022-09-24 16:16:56 +00:00
" vSize " : extTx . virtualSize ( ) ,
2022-08-26 08:11:35 +00:00
" jmintValue " : changeToMint ,
" publicCoin " : " jmintData.publicCoin " ,
" spendCoinIndexes " : spendCoinIndexes ,
" height " : locktime ,
" txType " : " Sent " ,
" confirmed_status " : false ,
2023-04-11 17:20:57 +00:00
" amount " : amountAmount . decimal . toDouble ( ) ,
" recipientAmt " : amountAmount ,
2022-08-26 08:11:35 +00:00
" address " : address ,
" timestamp " : DateTime . now ( ) . millisecondsSinceEpoch ~ / 1000 ,
" subType " : " join " ,
} ;
}
Future < int > getBlockHead ( ElectrumX client ) async {
try {
final tip = await client . getBlockHeadTip ( ) ;
return tip [ " height " ] as int ;
} catch ( e ) {
Logging . instance
. log ( " Exception rethrown in getBlockHead(): $ e " , level: LogLevel . Error ) ;
rethrow ;
}
}
// end of isolates
2023-02-03 22:34:06 +00:00
String constructDerivePath ( {
// required DerivePathType derivePathType,
required int networkWIF ,
int account = 0 ,
required int chain ,
required int index ,
} ) {
2022-08-26 08:11:35 +00:00
String coinType ;
2023-02-03 22:34:06 +00:00
switch ( networkWIF ) {
2022-08-26 08:11:35 +00:00
case 0xd2 : // firo mainnet wif
coinType = " 136 " ; // firo mainnet
break ;
case 0xb9 : // firo testnet wif
coinType = " 1 " ; // firo testnet
break ;
default :
2023-02-03 22:34:06 +00:00
throw Exception ( " Invalid Firo network wif used! " ) ;
2022-08-26 08:11:35 +00:00
}
2023-02-03 22:34:06 +00:00
int purpose ;
// switch (derivePathType) {
// case DerivePathType.bip44:
purpose = 44 ;
// break;
// default:
// throw Exception("DerivePathType $derivePathType not supported");
// }
2022-08-26 08:11:35 +00:00
2023-02-03 22:34:06 +00:00
return " m/ $ purpose '/ $ coinType '/ $ account '/ $ chain / $ index " ;
2022-08-26 08:11:35 +00:00
}
Future < String > _getMintScriptWrapper (
Tuple5 < int , String , int , String , bool > data ) async {
String mintHex = getMintScript ( data . item1 , data . item2 , data . item3 , data . item4 ,
isTestnet: data . item5 ) ;
return mintHex ;
}
Future < void > _setTestnetWrapper ( bool isTestnet ) async {
2022-08-29 13:50:02 +00:00
// setTestnet(isTestnet);
2022-08-26 08:11:35 +00:00
}
/// Handles a single instance of a firo wallet
2023-04-08 00:44:43 +00:00
class FiroWallet extends CoinServiceAPI
2023-07-24 22:49:11 +00:00
with WalletCache , WalletDB
2023-04-08 00:44:43 +00:00
implements XPubAble {
2023-02-03 22:48:16 +00:00
// Constructor
FiroWallet ( {
required String walletId ,
required String walletName ,
required Coin coin ,
required ElectrumX client ,
required CachedElectrumX cachedClient ,
required TransactionNotificationTracker tracker ,
required SecureStorageInterface secureStore ,
MainDB ? mockableOverride ,
} ) {
txTracker = tracker ;
_walletId = walletId ;
_walletName = walletName ;
_coin = coin ;
_electrumXClient = client ;
_cachedElectrumXClient = cachedClient ;
_secureStore = secureStore ;
initCache ( walletId , coin ) ;
initWalletDB ( mockableOverride: mockableOverride ) ;
Logging . instance . log ( " $ walletName isolates length: ${ isolates . length } " ,
level: LogLevel . Info ) ;
// investigate possible issues killing shared isolates between multiple firo instances
for ( final isolate in isolates . values ) {
isolate . kill ( priority: Isolate . immediate ) ;
}
isolates . clear ( ) ;
}
2022-08-26 08:11:35 +00:00
static const integrationTestFlag =
bool . fromEnvironment ( " IS_INTEGRATION_TEST " ) ;
final _prefs = Prefs . instance ;
Timer ? timer ;
2023-01-12 21:20:57 +00:00
late final Coin _coin ;
2022-08-26 08:11:35 +00:00
bool _shouldAutoSync = false ;
@ override
bool get shouldAutoSync = > _shouldAutoSync ;
@ override
set shouldAutoSync ( bool shouldAutoSync ) {
if ( _shouldAutoSync ! = shouldAutoSync ) {
_shouldAutoSync = shouldAutoSync ;
if ( ! shouldAutoSync ) {
timer ? . cancel ( ) ;
timer = null ;
stopNetworkAlivePinging ( ) ;
} else {
startNetworkAlivePinging ( ) ;
refresh ( ) ;
}
}
}
NetworkType get _network {
switch ( coin ) {
case Coin . firo:
return firoNetwork ;
case Coin . firoTestNet:
return firoTestNetwork ;
default :
throw Exception ( " Invalid network type! " ) ;
}
}
@ override
set isFavorite ( bool markFavorite ) {
2023-01-12 21:20:57 +00:00
_isFavorite = markFavorite ;
updateCachedIsFavorite ( markFavorite ) ;
2022-08-26 08:11:35 +00:00
}
@ override
2023-01-12 21:20:57 +00:00
bool get isFavorite = > _isFavorite ? ? = getCachedIsFavorite ( ) ;
bool ? _isFavorite ;
2022-08-26 08:11:35 +00:00
@ override
Coin get coin = > _coin ;
@ override
Future < List < String > > get mnemonic = > _getMnemonicList ( ) ;
2023-02-03 22:34:06 +00:00
@ override
Future < String ? > get mnemonicString = >
_secureStore . read ( key: ' ${ _walletId } _mnemonic ' ) ;
@ override
Future < String ? > get mnemonicPassphrase = > _secureStore . read (
key: ' ${ _walletId } _mnemonicPassphrase ' ,
) ;
2022-08-26 08:11:35 +00:00
@ override
bool validateAddress ( String address ) {
return Address . validateAddress ( address , _network ) ;
}
/// Holds wallet transaction data
2023-01-16 21:04:03 +00:00
Future < List < isar_models . Transaction > > get _txnData = > db
. getTransactions ( walletId )
2023-01-12 00:16:17 +00:00
. filter ( )
. isLelantusIsNull ( )
. or ( )
. isLelantusEqualTo ( false )
. findAll ( ) ;
2023-05-16 17:05:25 +00:00
2023-01-12 00:16:17 +00:00
// _transactionData ??= _refreshTransactions();
2022-08-26 08:11:35 +00:00
2023-01-12 00:16:17 +00:00
// models.TransactionData? cachedTxData;
2022-11-07 16:24:08 +00:00
// hack to add tx to txData before refresh completes
// required based on current app architecture where we don't properly store
// transactions locally in a good way
@ override
Future < void > updateSentCachedTxData ( Map < String , dynamic > txData ) async {
2023-02-02 22:19:14 +00:00
final transaction = isar_models . Transaction (
walletId: walletId ,
txid: txData [ " txid " ] as String ,
timestamp: DateTime . now ( ) . millisecondsSinceEpoch ~ / 1000 ,
type: isar_models . TransactionType . outgoing ,
subType: isar_models . TransactionSubType . none ,
2023-04-06 23:49:13 +00:00
// precision may be lost here hence the following amountString
2023-04-11 15:17:58 +00:00
amount: ( txData [ " recipientAmt " ] as Amount ) . raw . toInt ( ) ,
amountString: ( txData [ " recipientAmt " ] as Amount ) . toJsonString ( ) ,
2023-02-02 22:19:14 +00:00
fee: txData [ " fee " ] as int ,
height: null ,
isCancelled: false ,
isLelantus: false ,
otherData: null ,
slateId: null ,
2023-03-31 16:15:42 +00:00
nonce: null ,
2023-02-03 19:22:21 +00:00
inputs: [ ] ,
outputs: [ ] ,
2023-05-28 12:57:05 +00:00
numberOfMessages: null ,
2023-02-02 22:19:14 +00:00
) ;
final address = txData [ " address " ] is String
? await db . getAddress ( walletId , txData [ " address " ] as String )
: null ;
await db . addNewTransactionData (
[
2023-02-03 19:22:21 +00:00
Tuple2 ( transaction , address ) ,
2023-02-02 22:19:14 +00:00
] ,
walletId ,
) ;
2022-11-07 16:24:08 +00:00
}
2022-08-26 08:11:35 +00:00
/// Holds the max fee that can be sent
Future < int > ? _maxFee ;
2023-05-16 17:05:25 +00:00
2022-08-26 08:11:35 +00:00
@ override
Future < int > get maxFee = > _maxFee ? ? = _fetchMaxFee ( ) ;
Future < FeeObject > ? _feeObject ;
2023-05-16 17:05:25 +00:00
2022-08-26 08:11:35 +00:00
@ override
Future < FeeObject > get fees = > _feeObject ? ? = _getFees ( ) ;
@ override
2023-01-12 00:16:17 +00:00
Future < String > get currentReceivingAddress async = >
( await _currentReceivingAddress ) . value ;
2023-01-23 16:32:53 +00:00
Future < isar_models . Address > get _currentReceivingAddress async = >
( await db
. getAddresses ( walletId )
. filter ( )
. typeEqualTo ( isar_models . AddressType . p2pkh )
. subTypeEqualTo ( isar_models . AddressSubType . receiving )
. sortByDerivationIndexDesc ( )
. findFirst ( ) ) ? ?
await _generateAddressForChain ( 0 , 0 ) ;
2023-01-12 00:16:17 +00:00
Future < String > get currentChangeAddress async = >
( await _currentChangeAddress ) . value ;
2023-01-23 16:32:53 +00:00
Future < isar_models . Address > get _currentChangeAddress async = >
( await db
. getAddresses ( walletId )
. filter ( )
. typeEqualTo ( isar_models . AddressType . p2pkh )
. subTypeEqualTo ( isar_models . AddressSubType . change )
. sortByDerivationIndexDesc ( )
. findFirst ( ) ) ? ?
await _generateAddressForChain ( 1 , 0 ) ;
2022-08-26 08:11:35 +00:00
late String _walletName ;
2023-05-16 17:05:25 +00:00
2022-08-26 08:11:35 +00:00
@ override
String get walletName = > _walletName ;
// setter for updating on rename
@ override
set walletName ( String newName ) = > _walletName = newName ;
/// unique wallet id
2023-01-12 21:20:57 +00:00
late final String _walletId ;
2023-05-16 17:05:25 +00:00
2022-08-26 08:11:35 +00:00
@ override
String get walletId = > _walletId ;
@ override
Future < bool > testNetworkConnection ( ) async {
try {
final result = await _electrumXClient . ping ( ) ;
return result ;
} catch ( _ ) {
return false ;
}
}
Timer ? _networkAliveTimer ;
void startNetworkAlivePinging ( ) {
// call once on start right away
_periodicPingCheck ( ) ;
// then periodically check
_networkAliveTimer = Timer . periodic (
Constants . networkAliveTimerDuration ,
( _ ) async {
_periodicPingCheck ( ) ;
} ,
) ;
}
void _periodicPingCheck ( ) async {
bool hasNetwork = await testNetworkConnection ( ) ;
2023-05-29 16:04:18 +00:00
2022-08-26 08:11:35 +00:00
if ( _isConnected ! = hasNetwork ) {
NodeConnectionStatus status = hasNetwork
? NodeConnectionStatus . connected
: NodeConnectionStatus . disconnected ;
GlobalEventBus . instance
. fire ( NodeConnectionStatusChangedEvent ( status , walletId , coin ) ) ;
2023-05-29 16:04:18 +00:00
_isConnected = hasNetwork ;
if ( hasNetwork ) {
unawaited ( refresh ( ) ) ;
}
2022-08-26 08:11:35 +00:00
}
}
void stopNetworkAlivePinging ( ) {
_networkAliveTimer ? . cancel ( ) ;
_networkAliveTimer = null ;
}
bool _isConnected = false ;
@ override
bool get isConnected = > _isConnected ;
2022-09-07 15:56:10 +00:00
Future < Map < String , dynamic > > prepareSendPublic ( {
required String address ,
2023-04-05 22:06:31 +00:00
required Amount amount ,
2022-09-07 15:56:10 +00:00
Map < String , dynamic > ? args ,
} ) async {
try {
final feeRateType = args ? [ " feeRate " ] ;
2023-06-17 16:42:23 +00:00
final customSatsPerVByte = args ? [ " satsPerVByte " ] as int ? ;
2022-09-07 15:56:10 +00:00
final feeRateAmount = args ? [ " feeRateAmount " ] ;
2023-06-17 16:42:23 +00:00
if ( customSatsPerVByte ! = null ) {
// check for send all
bool isSendAll = false ;
if ( amount = = balance . spendable ) {
isSendAll = true ;
}
final result = await coinSelection (
amount . raw . toInt ( ) ,
- 1 ,
address ,
isSendAll ,
satsPerVByte: customSatsPerVByte ,
) ;
Logging . instance
. log ( " PREPARE SEND RESULT: $ result " , level: LogLevel . Info ) ;
if ( result is int ) {
switch ( result ) {
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 $ result " ) ;
}
} else {
final hex = result [ " hex " ] ;
if ( hex is String ) {
final fee = result [ " fee " ] as int ;
final vSize = result [ " vSize " ] as int ;
Logging . instance . log ( " txHex: $ hex " , level: LogLevel . Info ) ;
Logging . instance . log ( " fee: $ fee " , level: LogLevel . Info ) ;
Logging . instance . log ( " 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 result as Map < String , dynamic > ;
} else {
throw Exception ( " sent hex is not a String!!! " ) ;
}
}
} else if ( feeRateType is FeeRateType | | feeRateAmount is int ) {
2022-09-07 15:56:10 +00:00
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 ;
2023-06-17 16:42:23 +00:00
default :
throw ArgumentError ( " Invalid use of custom fee " ) ;
2022-09-07 15:56:10 +00:00
}
rate = fee ;
} else {
rate = feeRateAmount as int ;
}
// check for send all
bool isSendAll = false ;
2023-04-05 22:06:31 +00:00
final balance = availablePublicBalance ( ) ;
if ( amount = = balance ) {
2022-09-07 15:56:10 +00:00
isSendAll = true ;
}
2023-04-05 22:06:31 +00:00
final txData = await coinSelection (
amount . raw . toInt ( ) ,
rate ,
address ,
isSendAll ,
) ;
2022-09-07 15:56:10 +00:00
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 ) {
2023-07-25 20:56:31 +00:00
Logging . instance . log (
" Exception rethrown from prepareSendPublic(): $ e \n $ s " ,
2022-09-07 15:56:10 +00:00
level: LogLevel . Error ) ;
rethrow ;
}
} else {
throw ArgumentError ( " Invalid fee rate argument provided! " ) ;
}
} catch ( e , s ) {
2023-07-25 20:56:31 +00:00
Logging . instance . log (
" Exception rethrown from prepareSendPublic(): $ e \n $ s " ,
2022-09-07 15:56:10 +00:00
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 ) ;
2022-11-07 16:24:08 +00:00
txData [ " txid " ] = txHash ;
// dirty ui update hack
await updateSentCachedTxData ( txData as Map < String , dynamic > ) ;
2022-09-07 15:56:10 +00:00
return txHash ;
} catch ( e , s ) {
Logging . instance . log ( " Exception rethrown from confirmSend(): $ e \n $ s " ,
level: LogLevel . Error ) ;
rethrow ;
}
}
2022-08-26 08:11:35 +00:00
@ override
2022-09-07 15:56:10 +00:00
Future < Map < String , dynamic > > prepareSend ( {
required String address ,
2023-04-05 22:06:31 +00:00
required Amount amount ,
2022-09-07 15:56:10 +00:00
Map < String , dynamic > ? args ,
} ) async {
2022-08-26 08:11:35 +00:00
try {
2022-09-08 12:45:38 +00:00
// check for send all
bool isSendAll = false ;
2023-04-05 22:06:31 +00:00
final balance = availablePrivateBalance ( ) ;
if ( amount = = balance ) {
2022-09-09 17:14:38 +00:00
// print("is send all");
2022-09-08 12:45:38 +00:00
isSendAll = true ;
}
2023-04-05 22:06:31 +00:00
dynamic txHexOrError = await _createJoinSplitTransaction (
amount . raw . toInt ( ) ,
address ,
isSendAll ,
) ;
2022-08-26 08:11:35 +00:00
Logging . instance . log ( " txHexOrError $ txHexOrError " , level: LogLevel . Error ) ;
if ( txHexOrError is int ) {
// Here, we assume that transaction crafting returned an error
switch ( txHexOrError ) {
case 1 :
throw Exception ( " Insufficient balance! " ) ;
default :
throw Exception ( " Error Creating Transaction! " ) ;
}
} else {
2022-09-24 16:16:56 +00:00
final fee = txHexOrError [ " fee " ] as int ;
final vSize = txHexOrError [ " vSize " ] as int ;
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 " ) ;
}
2022-08-26 08:11:35 +00:00
return txHexOrError as Map < String , dynamic > ;
}
} catch ( e , s ) {
Logging . instance . log ( " Exception rethrown in firo prepareSend(): $ e \n $ s " ,
level: LogLevel . Error ) ;
rethrow ;
}
}
@ override
Future < String > confirmSend ( { required Map < String , dynamic > txData } ) async {
if ( await _submitLelantusToNetwork ( txData ) ) {
try {
final txid = txData [ " txid " ] as String ;
return txid ;
} catch ( e , s ) {
2022-12-13 00:17:02 +00:00
//todo: come back to this
2022-08-26 08:11:35 +00:00
debugPrint ( " $ e $ s " ) ;
return txData [ " txid " ] as String ;
// don't throw anything here or it will tell the user that th tx
// failed even though it was successfully broadcast to network
// throw Exception("Transaction failed.");
}
} else {
//TODO provide more info
throw Exception ( " Transaction failed. " ) ;
}
}
2023-01-12 00:16:17 +00:00
// /// returns txid on successful send
// ///
// /// can throw
// @override
// Future<String> send({
// required String toAddress,
// required int amount,
// Map<String, String> args = const {},
// }) async {
// try {
// dynamic txHexOrError =
// await _createJoinSplitTransaction(amount, toAddress, false);
// Logging.instance.log("txHexOrError $txHexOrError", level: LogLevel.Error);
// if (txHexOrError is int) {
// // Here, we assume that transaction crafting returned an error
// switch (txHexOrError) {
// case 1:
// throw Exception("Insufficient balance!");
// default:
// throw Exception("Error Creating Transaction!");
// }
// } else {
// if (await _submitLelantusToNetwork(
// txHexOrError as Map<String, dynamic>)) {
// final txid = txHexOrError["txid"] as String;
//
// // temporarily update apdate available balance until a full refresh is done
// Decimal sendTotal =
// Format.satoshisToAmount(txHexOrError["value"] as int, coin: coin);
// sendTotal += Decimal.parse(txHexOrError["fees"].toString());
// final bals = await balances;
// bals[0] -= sendTotal;
// _balances = Future(() => bals);
//
// return txid;
// } else {
// //TODO provide more info
// throw Exception("Transaction failed.");
// }
// }
// } catch (e, s) {
// Logging.instance.log("Exception rethrown in firo send(): $e\n$s",
// level: LogLevel.Error);
// rethrow;
// }
// }
2022-08-26 08:11:35 +00:00
Future < List < String > > _getMnemonicList ( ) async {
2023-02-03 22:34:06 +00:00
final _mnemonicString = await mnemonicString ;
if ( _mnemonicString = = null ) {
2022-08-26 08:11:35 +00:00
return [ ] ;
}
2023-02-03 22:34:06 +00:00
final List < String > data = _mnemonicString . split ( ' ' ) ;
2022-08-26 08:11:35 +00:00
return data ;
}
late ElectrumX _electrumXClient ;
2023-05-16 17:05:25 +00:00
2022-08-26 08:11:35 +00:00
ElectrumX get electrumXClient = > _electrumXClient ;
late CachedElectrumX _cachedElectrumXClient ;
2023-05-16 17:05:25 +00:00
2022-08-26 08:11:35 +00:00
CachedElectrumX get cachedElectrumXClient = > _cachedElectrumXClient ;
2022-11-09 23:48:43 +00:00
late SecureStorageInterface _secureStore ;
2022-08-26 08:11:35 +00:00
late TransactionNotificationTracker txTracker ;
2022-09-07 15:56:10 +00:00
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 , {
2023-06-17 16:42:23 +00:00
int ? satsPerVByte ,
2022-09-07 15:56:10 +00:00
int additionalOutputs = 0 ,
2023-01-12 00:16:17 +00:00
List < isar_models . UTXO > ? utxos ,
2022-09-07 15:56:10 +00:00
} ) async {
Logging . instance
. log ( " Starting coinSelection ---------- " , level: LogLevel . Info ) ;
2023-01-12 00:16:17 +00:00
final List < isar_models . UTXO > availableOutputs = utxos ? ? await this . utxos ;
final currentChainHeight = await chainHeight ;
final List < isar_models . UTXO > spendableOutputs = [ ] ;
2022-09-07 15:56:10 +00:00
int spendableSatoshiValue = 0 ;
// Build list of spendable outputs and totaling their satoshi amount
for ( var i = 0 ; i < availableOutputs . length ; i + + ) {
2023-01-12 00:16:17 +00:00
if ( availableOutputs [ i ] . isBlocked = = false & &
availableOutputs [ i ]
. isConfirmed ( currentChainHeight , MINIMUM_CONFIRMATIONS ) = =
true ) {
2022-09-07 15:56:10 +00:00
spendableOutputs . add ( availableOutputs [ i ] ) ;
spendableSatoshiValue + = availableOutputs [ i ] . value ;
}
}
// sort spendable by age (oldest first)
2023-01-12 00:16:17 +00:00
spendableOutputs . sort ( ( a , b ) = > b . blockTime ! . compareTo ( a . blockTime ! ) ) ;
2022-09-07 15:56:10 +00:00
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 ;
2023-01-12 00:16:17 +00:00
List < isar_models . UTXO > utxoObjectsToUse = [ ] ;
2022-09-07 15:56:10 +00:00
for ( var i = 0 ;
2022-10-18 19:15:57 +00:00
satoshisBeingUsed < = satoshiAmountToSend & & i < spendableOutputs . length ;
2022-09-07 15:56:10 +00:00
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 (
utxoSigningData: utxoSigningData ,
recipients: [ _recipientAddress ] ,
satoshiAmounts: [ satoshisBeingUsed - 1 ] ,
) ) [ " vSize " ] as int ;
2023-06-17 16:42:23 +00:00
int feeForOneOutput = satsPerVByte ! = null
? ( satsPerVByte * vSizeForOneOutput )
: estimateTxFee (
vSize: vSizeForOneOutput ,
feeRatePerKB: selectedTxFeeRate ,
) ;
2022-09-07 15:56:10 +00:00
2023-07-25 17:17:30 +00:00
int amount = satoshiAmountToSend - feeForOneOutput ;
2022-09-07 15:56:10 +00:00
dynamic txn = await buildTransaction (
utxoSigningData: utxoSigningData ,
recipients: recipientsArray ,
satoshiAmounts: [ amount ] ,
) ;
2023-07-25 17:17:30 +00:00
int count = 0 ;
int fee = feeForOneOutput ;
int vsize = txn [ " vSize " ] as int ;
while ( fee < vsize & & count < 10 ) {
// 10 being some reasonable max
count + + ;
fee + = count ;
amount = satoshiAmountToSend - fee ;
txn = await buildTransaction (
utxoSigningData: utxoSigningData ,
recipients: recipientsArray ,
satoshiAmounts: [ amount ] ,
) ;
vsize = txn [ " vSize " ] as int ;
}
2022-09-07 15:56:10 +00:00
Map < String , dynamic > transactionObject = {
" hex " : txn [ " hex " ] ,
" recipient " : recipientsArray [ 0 ] ,
2023-04-11 15:17:58 +00:00
" recipientAmt " : Amount (
rawValue: BigInt . from ( amount ) ,
fractionDigits: coin . decimals ,
) ,
2022-09-07 15:56:10 +00:00
" fee " : feeForOneOutput ,
" vSize " : txn [ " vSize " ] ,
} ;
return transactionObject ;
}
final int vSizeForOneOutput = ( await buildTransaction (
utxoSigningData: utxoSigningData ,
recipients: [ _recipientAddress ] ,
satoshiAmounts: [ satoshisBeingUsed - 1 ] ,
) ) [ " vSize " ] as int ;
final int vSizeForTwoOutPuts = ( await buildTransaction (
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 ;
// Assume 1 output, only for recipient and no change
2023-06-17 16:42:23 +00:00
var feeForOneOutput = satsPerVByte ! = null
? ( satsPerVByte * vSizeForOneOutput )
: estimateTxFee (
vSize: vSizeForOneOutput ,
feeRatePerKB: selectedTxFeeRate ,
) ;
2022-09-07 15:56:10 +00:00
// Assume 2 outputs, one for recipient and one for change
2023-06-17 16:42:23 +00:00
var feeForTwoOutputs = satsPerVByte ! = null
? ( satsPerVByte * vSizeForTwoOutPuts )
: estimateTxFee (
vSize: vSizeForTwoOutPuts ,
feeRatePerKB: selectedTxFeeRate ,
) ;
2022-09-07 15:56:10 +00:00
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 (
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 (
utxoSigningData: utxoSigningData ,
recipients: recipientsArray ,
satoshiAmounts: recipientsAmtArray ,
) ;
}
Map < String , dynamic > transactionObject = {
" hex " : txn [ " hex " ] ,
" recipient " : recipientsArray [ 0 ] ,
2023-04-11 15:17:58 +00:00
" recipientAmt " : Amount (
rawValue: BigInt . from ( recipientsAmtArray [ 0 ] ) ,
fractionDigits: coin . decimals ,
) ,
2022-09-07 15:56:10 +00:00
" 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 (
utxoSigningData: utxoSigningData ,
recipients: recipientsArray ,
satoshiAmounts: recipientsAmtArray ,
) ;
Map < String , dynamic > transactionObject = {
" hex " : txn [ " hex " ] ,
" recipient " : recipientsArray [ 0 ] ,
2023-04-11 15:17:58 +00:00
" recipientAmt " : Amount (
rawValue: BigInt . from ( recipientsAmtArray [ 0 ] ) ,
fractionDigits: coin . decimals ,
) ,
2022-09-07 15:56:10 +00:00
" 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 (
utxoSigningData: utxoSigningData ,
recipients: recipientsArray ,
satoshiAmounts: recipientsAmtArray ,
) ;
Map < String , dynamic > transactionObject = {
" hex " : txn [ " hex " ] ,
" recipient " : recipientsArray [ 0 ] ,
2023-04-11 15:17:58 +00:00
" recipientAmt " : Amount (
rawValue: BigInt . from ( recipientsAmtArray [ 0 ] ) ,
fractionDigits: coin . decimals ,
) ,
2022-09-07 15:56:10 +00:00
" 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 (
utxoSigningData: utxoSigningData ,
recipients: recipientsArray ,
satoshiAmounts: recipientsAmtArray ,
) ;
Map < String , dynamic > transactionObject = {
" hex " : txn [ " hex " ] ,
" recipient " : recipientsArray [ 0 ] ,
2023-04-11 15:17:58 +00:00
" recipientAmt " : Amount (
rawValue: BigInt . from ( recipientsAmtArray [ 0 ] ) ,
fractionDigits: coin . decimals ,
) ,
2022-09-07 15:56:10 +00:00
" 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 ) {
2023-06-17 16:42:23 +00:00
return coinSelection (
satoshiAmountToSend ,
selectedTxFeeRate ,
_recipientAddress ,
isSendAll ,
additionalOutputs: additionalOutputs + 1 ,
satsPerVByte: satsPerVByte ,
utxos: utxos ,
) ;
2022-09-07 15:56:10 +00:00
}
return 2 ;
}
}
2023-03-09 19:49:39 +00:00
Future < List < SigningData > > fetchBuildTxData (
2023-01-12 00:16:17 +00:00
List < isar_models . UTXO > utxosToUse ,
2022-09-07 15:56:10 +00:00
) async {
// return data
2023-03-09 19:49:39 +00:00
List < SigningData > signingData = [ ] ;
2022-09-07 15:56:10 +00:00
try {
// Populating the addresses to check
for ( var i = 0 ; i < utxosToUse . length ; i + + ) {
2023-03-09 19:49:39 +00:00
if ( utxosToUse [ i ] . address = = null ) {
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 ) {
utxosToUse [ i ] = utxosToUse [ i ] . copyWith (
address: output [ " scriptPubKey " ] ? [ " addresses " ] ? [ 0 ] as String ? ? ?
output [ " scriptPubKey " ] [ " address " ] as String ,
) ;
2022-09-07 15:56:10 +00:00
}
}
}
2023-03-09 19:49:39 +00:00
signingData . add (
SigningData (
derivePathType: DerivePathType . bip44 ,
utxo: utxosToUse [ i ] ,
) ,
) ;
}
2022-09-07 15:56:10 +00:00
2023-03-09 19:49:39 +00:00
Map < DerivePathType , Map < String , dynamic > > receiveDerivations = { } ;
Map < DerivePathType , Map < String , dynamic > > changeDerivations = { } ;
2022-09-07 15:56:10 +00:00
2023-03-09 19:49:39 +00:00
for ( final sd in signingData ) {
String ? pubKey ;
String ? wif ;
2022-09-07 15:56:10 +00:00
2023-05-16 17:05:25 +00:00
final address = await db . getAddress ( walletId , sd . utxo . address ! ) ;
if ( address ? . derivationPath ! = null ) {
final node = await Bip32Utils . getBip32Node (
( await mnemonicString ) ! ,
( await mnemonicPassphrase ) ! ,
_network ,
address ! . derivationPath ! . value ,
) ;
2022-09-07 15:56:10 +00:00
2023-05-16 17:05:25 +00:00
wif = node . toWIF ( ) ;
pubKey = Format . uint8listToString ( node . publicKey ) ;
2023-03-09 19:49:39 +00:00
}
2023-05-16 17:05:25 +00:00
if ( wif = = null | | pubKey = = null ) {
// fetch receiving derivations if null
receiveDerivations [ sd . derivePathType ] ? ? = Map < String , dynamic > . from (
2023-03-09 19:49:39 +00:00
jsonDecode ( ( await _secureStore . read (
2023-05-16 17:05:25 +00:00
key: " ${ walletId } _receiveDerivations " ,
2023-03-09 19:49:39 +00:00
) ) ? ?
" {} " ) as Map ,
) ;
2023-05-16 17:05:25 +00:00
dynamic receiveDerivation ;
2023-03-09 19:49:39 +00:00
for ( int j = 0 ;
2023-05-16 17:05:25 +00:00
j < receiveDerivations [ sd . derivePathType ] ! . length & &
receiveDerivation = = null ;
2023-03-09 19:49:39 +00:00
j + + ) {
2023-05-16 17:05:25 +00:00
if ( receiveDerivations [ sd . derivePathType ] ! [ " $ j " ] [ " address " ] = =
2023-03-09 19:49:39 +00:00
sd . utxo . address ! ) {
2023-05-16 17:05:25 +00:00
receiveDerivation = receiveDerivations [ sd . derivePathType ] ! [ " $ j " ] ;
2022-09-07 15:56:10 +00:00
}
2023-03-09 19:49:39 +00:00
}
2022-09-07 15:56:10 +00:00
2023-05-16 17:05:25 +00:00
if ( receiveDerivation ! = null ) {
pubKey = receiveDerivation [ " publicKey " ] as String ;
wif = receiveDerivation [ " wif " ] as String ;
} else {
// fetch change derivations if null
changeDerivations [ sd . derivePathType ] ? ? = Map < String , dynamic > . from (
jsonDecode ( ( await _secureStore . read (
key: " ${ walletId } _changeDerivations " ,
) ) ? ?
" {} " ) as Map ,
2023-03-09 21:08:13 +00:00
) ;
2023-05-16 17:05:25 +00:00
dynamic changeDerivation ;
for ( int j = 0 ;
j < changeDerivations [ sd . derivePathType ] ! . length & &
changeDerivation = = null ;
j + + ) {
if ( changeDerivations [ sd . derivePathType ] ! [ " $ j " ] [ " address " ] = =
sd . utxo . address ! ) {
changeDerivation = changeDerivations [ sd . derivePathType ] ! [ " $ j " ] ;
}
}
if ( changeDerivation ! = null ) {
pubKey = changeDerivation [ " publicKey " ] as String ;
wif = changeDerivation [ " wif " ] as String ;
}
2023-03-09 21:08:13 +00:00
}
}
2023-03-09 19:49:39 +00:00
if ( wif ! = null & & pubKey ! = null ) {
final PaymentData data ;
final Uint8List ? redeemScript ;
2022-09-07 15:56:10 +00:00
2023-03-09 19:49:39 +00:00
switch ( sd . derivePathType ) {
case DerivePathType . bip44:
data = P2PKH (
2022-09-07 15:56:10 +00:00
data: PaymentData (
2023-03-09 19:49:39 +00:00
pubkey: Format . stringToUint8List ( pubKey ) ,
) ,
2022-09-07 15:56:10 +00:00
network: _network ,
) . data ;
2023-03-09 19:49:39 +00:00
redeemScript = null ;
break ;
2022-09-07 15:56:10 +00:00
2023-03-09 19:49:39 +00:00
default :
throw Exception ( " DerivePathType unsupported " ) ;
2022-09-07 15:56:10 +00:00
}
2023-03-09 19:49:39 +00:00
final keyPair = ECPair . fromWIF (
wif ,
network: _network ,
) ;
sd . redeemScript = redeemScript ;
sd . output = data . output ;
sd . keyPair = keyPair ;
2023-05-16 17:05:25 +00:00
} else {
throw Exception ( " key or wif not found for ${ sd . utxo } " ) ;
2022-09-07 15:56:10 +00:00
}
}
2023-03-09 19:49:39 +00:00
return signingData ;
2022-09-07 15:56:10 +00:00
} 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 ( {
2023-03-09 19:49:39 +00:00
required List < SigningData > utxoSigningData ,
2022-09-07 15:56:10 +00:00
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
2023-03-09 19:49:39 +00:00
for ( var i = 0 ; i < utxoSigningData . length ; i + + ) {
final txid = utxoSigningData [ i ] . utxo . txid ;
txb . addInput (
txid ,
utxoSigningData [ i ] . utxo . vout ,
null ,
utxoSigningData [ i ] . output ! ,
) ;
2022-09-07 15:56:10 +00:00
}
// Add transaction output
for ( var i = 0 ; i < recipients . length ; i + + ) {
txb . addOutput ( recipients [ i ] , satoshiAmounts [ i ] ) ;
}
try {
// Sign the transaction accordingly
2023-03-09 19:49:39 +00:00
for ( var i = 0 ; i < utxoSigningData . length ; i + + ) {
2022-09-07 15:56:10 +00:00
txb . sign (
vin: i ,
2023-03-09 19:49:39 +00:00
keyPair: utxoSigningData [ i ] . keyPair ! ,
witnessValue: utxoSigningData [ i ] . utxo . value ,
redeemScript: utxoSigningData [ i ] . redeemScript ,
2022-09-07 15:56:10 +00:00
) ;
}
} 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 } ;
}
2022-08-26 08:11:35 +00:00
@ override
Future < void > updateNode ( bool shouldRefresh ) async {
2022-11-09 22:43:26 +00:00
final failovers = NodeService ( secureStorageInterface: _secureStore )
2022-08-26 08:11:35 +00:00
. failoverNodesFor ( coin: coin )
. map (
( e ) = > ElectrumXNode (
address: e . host ,
port: e . port ,
name: e . name ,
id: e . id ,
useSSL: e . useSSL ,
) ,
)
. toList ( ) ;
final newNode = await _getCurrentNode ( ) ;
_electrumXClient = ElectrumX . from (
node: newNode ,
prefs: _prefs ,
failovers: failovers ,
) ;
2023-05-25 20:37:18 +00:00
_cachedElectrumXClient = CachedElectrumX . from (
electrumXClient: _electrumXClient ,
) ;
2022-08-26 08:11:35 +00:00
if ( shouldRefresh ) {
2022-09-06 15:34:39 +00:00
unawaited ( refresh ( ) ) ;
2022-08-26 08:11:35 +00:00
}
}
@ override
Future < void > initializeNew ( ) async {
Logging . instance
. log ( " Generating new ${ coin . prettyName } wallet. " , level: LogLevel . Info ) ;
2023-01-12 21:20:57 +00:00
if ( getCachedId ( ) ! = null ) {
2022-08-26 08:11:35 +00:00
throw Exception (
" Attempted to initialize a new wallet using an existing wallet ID! " ) ;
}
await _prefs . init ( ) ;
try {
await _generateNewWallet ( ) ;
} catch ( e , s ) {
Logging . instance . log ( " Exception rethrown from initializeNew(): $ e \n $ s " ,
level: LogLevel . Fatal ) ;
rethrow ;
}
await Future . wait ( [
2023-01-12 21:20:57 +00:00
updateCachedId ( walletId ) ,
updateCachedIsFavorite ( false ) ,
2022-08-26 08:11:35 +00:00
] ) ;
}
@ override
Future < void > initializeExisting ( ) async {
Logging . instance . log (
2023-02-13 18:13:30 +00:00
" initializeExisting() $ _walletId ${ coin . prettyName } wallet. " ,
2022-08-26 08:11:35 +00:00
level: LogLevel . Info ) ;
2023-01-12 21:20:57 +00:00
if ( getCachedId ( ) = = null ) {
2022-08-26 08:11:35 +00:00
throw Exception (
" Attempted to initialize an existing wallet using an unknown wallet ID! " ) ;
}
await _prefs . init ( ) ;
2023-02-13 18:13:30 +00:00
// await checkChangeAddressForTransactions();
// await checkReceivingAddressForTransactions();
2022-08-26 08:11:35 +00:00
}
Future < bool > refreshIfThereIsNewData ( ) async {
if ( longMutex ) return false ;
if ( _hasCalledExit ) return false ;
Logging . instance
. log ( " $ walletName refreshIfThereIsNewData " , level: LogLevel . Info ) ;
try {
bool needsRefresh = false ;
Set < String > txnsToCheck = { } ;
for ( final String txid in txTracker . pendings ) {
if ( ! txTracker . wasNotifiedConfirmed ( txid ) ) {
txnsToCheck . add ( txid ) ;
}
}
for ( String txid in txnsToCheck ) {
final txn = await electrumXClient . getTransaction ( txHash: txid ) ;
int confirmations = txn [ " confirmations " ] as int ? ? ? 0 ;
bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS ;
if ( ! isUnconfirmed ) {
needsRefresh = true ;
break ;
}
}
if ( ! needsRefresh ) {
2023-01-12 00:16:17 +00:00
final allOwnAddresses = await _fetchAllOwnAddresses ( ) ;
List < Map < String , dynamic > > allTxs = await _fetchHistory (
allOwnAddresses . map ( ( e ) = > e . value ) . toList ( growable: false ) ) ;
2022-08-26 08:11:35 +00:00
for ( Map < String , dynamic > transaction in allTxs ) {
2023-01-12 00:16:17 +00:00
final txid = transaction [ ' tx_hash ' ] as String ;
2023-01-16 21:04:03 +00:00
if ( ( await db
. getTransactions ( walletId )
2023-01-12 00:16:17 +00:00
. filter ( )
2023-05-17 17:34:40 +00:00
. txidEqualTo ( txid )
. count ( ) ) = =
0 ) {
2022-08-26 08:11:35 +00:00
Logging . instance . log (
2023-05-17 17:34:40 +00:00
" txid not found in address history already ${ transaction [ ' tx_hash ' ] } " ,
level: LogLevel . Info ,
) ;
2022-08-26 08:11:35 +00:00
needsRefresh = true ;
break ;
}
}
}
return needsRefresh ;
} catch ( e , s ) {
Logging . instance . log (
" Exception caught in refreshIfThereIsNewData: $ e \n $ s " ,
level: LogLevel . Error ) ;
rethrow ;
}
}
2023-01-12 00:16:17 +00:00
Future < void > getAllTxsToWatch ( ) async {
2022-08-26 08:11:35 +00:00
if ( _hasCalledExit ) return ;
Logging . instance . log ( " $ walletName periodic " , level: LogLevel . Info ) ;
2023-01-12 00:16:17 +00:00
List < isar_models . Transaction > unconfirmedTxnsToNotifyPending = [ ] ;
List < isar_models . Transaction > unconfirmedTxnsToNotifyConfirmed = [ ] ;
final currentChainHeight = await chainHeight ;
2023-05-17 17:34:40 +00:00
final txCount = await db . getTransactions ( walletId ) . count ( ) ;
2023-01-12 00:16:17 +00:00
2023-05-17 17:34:40 +00:00
const paginateLimit = 50 ;
2023-01-12 00:16:17 +00:00
2023-05-17 17:34:40 +00:00
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 ) ) {
2023-01-12 00:16:17 +00:00
// get all transactions that were notified as pending but not as confirmed
2023-05-17 17:34:40 +00:00
if ( txTracker . wasNotifiedPending ( tx . txid ) & &
! txTracker . wasNotifiedConfirmed ( tx . txid ) ) {
unconfirmedTxnsToNotifyConfirmed . add ( tx ) ;
2023-01-12 00:16:17 +00:00
}
} else {
2023-05-17 17:34:40 +00:00
// get all transactions that were not notified as pending yet
if ( ! txTracker . wasNotifiedPending ( tx . txid ) ) {
unconfirmedTxnsToNotifyPending . add ( tx ) ;
2022-08-26 08:11:35 +00:00
}
}
}
}
2023-01-12 00:16:17 +00:00
2022-08-26 08:11:35 +00:00
Logging . instance . log (
" unconfirmedTxnsToNotifyPending $ unconfirmedTxnsToNotifyPending " ,
level: LogLevel . Info ) ;
Logging . instance . log (
" unconfirmedTxnsToNotifyConfirmed $ unconfirmedTxnsToNotifyConfirmed " ,
level: LogLevel . Info ) ;
for ( final tx in unconfirmedTxnsToNotifyPending ) {
2023-01-12 00:16:17 +00:00
final confirmations = tx . getConfirmations ( currentChainHeight ) ;
switch ( tx . type ) {
case isar_models . TransactionType . incoming:
2023-05-09 17:54:15 +00:00
CryptoNotificationsEventBus . instance . fire (
CryptoNotificationEvent (
2022-09-06 15:34:39 +00:00
title: " Incoming transaction " ,
walletId: walletId ,
date: DateTime . fromMillisecondsSinceEpoch ( tx . timestamp * 1000 ) ,
2023-01-12 00:16:17 +00:00
shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS ,
2022-09-06 15:34:39 +00:00
txid: tx . txid ,
2023-01-12 00:16:17 +00:00
confirmations: confirmations ,
2022-09-06 15:34:39 +00:00
requiredConfirmations: MINIMUM_CONFIRMATIONS ,
2023-05-09 17:54:15 +00:00
walletName: walletName ,
coin: coin ,
2022-09-06 15:34:39 +00:00
) ,
2022-08-26 08:11:35 +00:00
) ;
2023-05-09 17:54:15 +00:00
2022-08-26 08:11:35 +00:00
await txTracker . addNotifiedPending ( tx . txid ) ;
break ;
2023-01-12 00:16:17 +00:00
case isar_models . TransactionType . outgoing:
2023-05-09 17:54:15 +00:00
CryptoNotificationsEventBus . instance . fire (
CryptoNotificationEvent (
2023-01-12 00:16:17 +00:00
title: tx . subType = = isar_models . TransactionSubType . mint
? " Anonymizing "
: " Outgoing transaction " ,
2022-09-06 15:34:39 +00:00
walletId: walletId ,
date: DateTime . fromMillisecondsSinceEpoch ( tx . timestamp * 1000 ) ,
2023-01-12 00:16:17 +00:00
shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS ,
2022-09-06 15:34:39 +00:00
txid: tx . txid ,
2023-01-12 00:16:17 +00:00
confirmations: confirmations ,
2022-09-06 15:34:39 +00:00
requiredConfirmations: MINIMUM_CONFIRMATIONS ,
2023-05-09 17:54:15 +00:00
walletName: walletName ,
coin: coin ,
2022-09-06 15:34:39 +00:00
) ,
2022-08-26 08:11:35 +00:00
) ;
2023-05-09 17:54:15 +00:00
2022-08-26 08:11:35 +00:00
await txTracker . addNotifiedPending ( tx . txid ) ;
break ;
default :
break ;
}
}
for ( final tx in unconfirmedTxnsToNotifyConfirmed ) {
2023-01-12 00:16:17 +00:00
if ( tx . type = = isar_models . TransactionType . incoming ) {
2023-05-09 17:54:15 +00:00
CryptoNotificationsEventBus . instance . fire (
CryptoNotificationEvent (
2022-09-06 15:34:39 +00:00
title: " Incoming transaction confirmed " ,
walletId: walletId ,
date: DateTime . fromMillisecondsSinceEpoch ( tx . timestamp * 1000 ) ,
shouldWatchForUpdates: false ,
2023-05-09 17:54:15 +00:00
txid: tx . txid ,
requiredConfirmations: MINIMUM_CONFIRMATIONS ,
walletName: walletName ,
coin: coin ,
2022-09-06 15:34:39 +00:00
) ,
2022-08-26 08:11:35 +00:00
) ;
2023-05-09 17:54:15 +00:00
2022-08-26 08:11:35 +00:00
await txTracker . addNotifiedConfirmed ( tx . txid ) ;
2023-01-12 00:16:17 +00:00
} else if ( tx . type = = isar_models . TransactionType . outgoing & &
tx . subType = = isar_models . TransactionSubType . join ) {
2023-05-09 17:54:15 +00:00
CryptoNotificationsEventBus . instance . fire (
CryptoNotificationEvent (
2023-01-12 00:16:17 +00:00
title: tx . subType = =
isar_models . TransactionSubType . mint // redundant check?
2022-10-15 17:57:00 +00:00
? " Anonymized "
: " Outgoing transaction confirmed " ,
2022-09-06 15:34:39 +00:00
walletId: walletId ,
date: DateTime . fromMillisecondsSinceEpoch ( tx . timestamp * 1000 ) ,
shouldWatchForUpdates: false ,
2023-05-09 17:54:15 +00:00
txid: tx . txid ,
requiredConfirmations: MINIMUM_CONFIRMATIONS ,
walletName: walletName ,
coin: coin ,
2022-09-06 15:34:39 +00:00
) ,
2022-08-26 08:11:35 +00:00
) ;
await txTracker . addNotifiedConfirmed ( tx . txid ) ;
}
}
}
/// Generates initial wallet values such as mnemonic, chain (receive/change) arrays and indexes.
Future < void > _generateNewWallet ( ) async {
2022-11-30 15:46:28 +00:00
Logging . instance
. log ( " IS_INTEGRATION_TEST: $ integrationTestFlag " , level: LogLevel . Info ) ;
if ( ! integrationTestFlag ) {
2022-11-30 15:54:46 +00:00
try {
final features = await electrumXClient
. getServerFeatures ( )
. timeout ( const Duration ( seconds: 3 ) ) ;
Logging . instance . log ( " features: $ features " , level: LogLevel . Info ) ;
switch ( coin ) {
case Coin . firo:
if ( features [ ' genesis_hash ' ] ! = GENESIS_HASH_MAINNET ) {
throw Exception ( " genesis hash does not match main net! " ) ;
}
break ;
case Coin . firoTestNet:
if ( features [ ' genesis_hash ' ] ! = GENESIS_HASH_TESTNET ) {
throw Exception ( " genesis hash does not match test net! " ) ;
}
break ;
default :
throw Exception (
" Attempted to generate a FiroWallet using a non firo coin type: ${ coin . name } " ) ;
}
} catch ( e , s ) {
Logging . instance . log ( " $ e /n $ s " , level: LogLevel . Info ) ;
2022-11-30 15:46:28 +00:00
}
}
2022-08-26 08:11:35 +00:00
// this should never fail as overwriting a mnemonic is big bad
2023-02-03 22:34:06 +00:00
if ( ( await mnemonicString ) ! = null | | ( await mnemonicPassphrase ) ! = null ) {
longMutex = false ;
throw Exception ( " Attempted to overwrite mnemonic on initialize new! " ) ;
}
2022-08-26 08:11:35 +00:00
await _secureStore . write (
key: ' ${ _walletId } _mnemonic ' ,
2023-05-19 20:05:58 +00:00
value: bip39 . generateMnemonic ( strength: 128 ) ) ;
2023-02-03 22:34:06 +00:00
await _secureStore . write (
key: ' ${ _walletId } _mnemonicPassphrase ' ,
value: " " ,
) ;
2022-08-26 08:11:35 +00:00
// Generate and add addresses to relevant arrays
final initialReceivingAddress = await _generateAddressForChain ( 0 , 0 ) ;
final initialChangeAddress = await _generateAddressForChain ( 1 , 0 ) ;
2023-01-12 00:16:17 +00:00
2023-01-16 21:04:03 +00:00
await db . putAddresses ( [
initialReceivingAddress ,
initialChangeAddress ,
] ) ;
2022-08-26 08:11:35 +00:00
}
bool refreshMutex = false ;
2023-05-16 17:05:25 +00:00
2022-08-26 08:11:35 +00:00
@ override
bool get isRefreshing = > refreshMutex ;
/// Refreshes display data for the wallet
@ override
Future < void > refresh ( ) async {
if ( refreshMutex ) {
2022-08-31 17:49:19 +00:00
Logging . instance . log ( " $ walletId $ walletName refreshMutex denied " ,
level: LogLevel . Info ) ;
2022-08-26 08:11:35 +00:00
return ;
} else {
refreshMutex = true ;
}
Logging . instance
. log ( " PROCESSORS ${ Platform . numberOfProcessors } " , level: LogLevel . Info ) ;
try {
GlobalEventBus . instance . fire (
WalletSyncStatusChangedEvent (
WalletSyncStatus . syncing ,
walletId ,
coin ,
) ,
) ;
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.0 , walletId ) ) ;
await checkReceivingAddressForTransactions ( ) ;
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.1 , walletId ) ) ;
2023-01-12 00:16:17 +00:00
await _refreshUTXOs ( ) ;
2022-08-26 08:11:35 +00:00
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.2 , walletId ) ) ;
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.25 , walletId ) ) ;
2023-01-12 00:16:17 +00:00
await _refreshTransactions ( ) ;
2022-08-26 08:11:35 +00:00
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.35 , walletId ) ) ;
2022-09-09 15:07:27 +00:00
final feeObj = _getFees ( ) ;
2022-08-26 08:11:35 +00:00
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.50 , walletId ) ) ;
_feeObject = Future ( ( ) = > feeObj ) ;
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.60 , walletId ) ) ;
2023-07-24 18:30:01 +00:00
// final lelantusCoins = getLelantusCoinMap();
// Logging.instance.log("_lelantus_coins at refresh: $lelantusCoins",
// level: LogLevel.Warning, printFullLength: true);
2022-08-26 08:11:35 +00:00
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.70 , walletId ) ) ;
await _refreshLelantusData ( ) ;
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.80 , walletId ) ) ;
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.90 , walletId ) ) ;
2023-01-12 00:16:17 +00:00
await _refreshBalance ( ) ;
2022-08-26 08:11:35 +00:00
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.95 , walletId ) ) ;
2023-01-12 00:16:17 +00:00
await getAllTxsToWatch ( ) ;
2022-08-26 08:11:35 +00:00
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 1.0 , walletId ) ) ;
GlobalEventBus . instance . fire (
WalletSyncStatusChangedEvent (
WalletSyncStatus . synced ,
walletId ,
coin ,
) ,
) ;
refreshMutex = false ;
if ( isActive | | shouldAutoSync ) {
2022-09-09 15:07:27 +00:00
timer ? ? = Timer . periodic ( const Duration ( seconds: 30 ) , ( timer ) async {
2022-08-26 08:11:35 +00:00
bool shouldNotify = await refreshIfThereIsNewData ( ) ;
if ( shouldNotify ) {
await refresh ( ) ;
GlobalEventBus . instance . fire ( UpdatedInBackgroundEvent (
2022-08-31 17:49:19 +00:00
" New data found in $ walletId $ walletName in background! " ,
walletId ) ) ;
2022-08-26 08:11:35 +00:00
}
} ) ;
}
} catch ( error , strace ) {
refreshMutex = false ;
GlobalEventBus . instance . fire (
NodeConnectionStatusChangedEvent (
NodeConnectionStatus . disconnected ,
walletId ,
coin ,
) ,
) ;
GlobalEventBus . instance . fire (
WalletSyncStatusChangedEvent (
WalletSyncStatus . unableToSync ,
walletId ,
coin ,
) ,
) ;
Logging . instance . log (
" Caught exception in refreshWalletData(): $ error \n $ strace " ,
level: LogLevel . Warning ) ;
}
}
Future < int > _fetchMaxFee ( ) async {
2023-01-12 00:16:17 +00:00
final balance = availablePrivateBalance ( ) ;
2023-04-05 22:06:31 +00:00
int spendAmount =
2023-05-24 15:56:44 +00:00
( balance . decimal * Decimal . fromInt ( Constants . satsPerCoin ( coin ) . toInt ( ) ) )
2023-04-05 22:06:31 +00:00
. toBigInt ( )
. toInt ( ) ;
2022-09-06 15:34:39 +00:00
int fee = await estimateJoinSplitFee ( spendAmount ) ;
2022-08-26 08:11:35 +00:00
return fee ;
}
Future < List < DartLelantusEntry > > _getLelantusEntry ( ) async {
2023-02-03 22:34:06 +00:00
final _mnemonic = await mnemonicString ;
final _mnemonicPassphrase = await mnemonicPassphrase ;
2023-02-13 22:53:28 +00:00
if ( _mnemonicPassphrase = = null ) {
Logging . instance . log (
" Exception in _getLelantusEntry: mnemonic passphrase null, possible migration issue; if using internal builds, delete wallet and restore from seed, if using a release build, please file bug report " ,
level: LogLevel . Error ) ;
}
2023-02-03 22:34:06 +00:00
2023-07-24 18:30:01 +00:00
final List < isar_models . LelantusCoin > lelantusCoins =
await _getUnspentCoins ( ) ;
2023-02-03 22:34:06 +00:00
final root = await Bip32Utils . getBip32Root (
_mnemonic ! ,
_mnemonicPassphrase ! ,
_network ,
2022-08-26 08:11:35 +00:00
) ;
2023-02-03 22:34:06 +00:00
2022-08-26 08:11:35 +00:00
final waitLelantusEntries = lelantusCoins . map ( ( coin ) async {
2023-02-03 22:34:06 +00:00
final derivePath = constructDerivePath (
networkWIF: _network . wif ,
chain: MINT_INDEX ,
2023-07-24 21:42:45 +00:00
index: coin . mintIndex ,
2022-08-26 08:11:35 +00:00
) ;
2023-02-03 22:34:06 +00:00
final keyPair = await Bip32Utils . getBip32NodeFromRoot ( root , derivePath ) ;
2022-08-26 08:11:35 +00:00
if ( keyPair . privateKey = = null ) {
Logging . instance . log ( " error bad key " , level: LogLevel . Error ) ;
return DartLelantusEntry ( 1 , 0 , 0 , 0 , 0 , ' ' ) ;
}
final String privateKey = Format . uint8listToString ( keyPair . privateKey ! ) ;
return DartLelantusEntry ( coin . isUsed ? 1 : 0 , 0 , coin . anonymitySetId ,
2023-07-24 21:42:45 +00:00
int . parse ( coin . value ) , coin . mintIndex , privateKey ) ;
2022-08-26 08:11:35 +00:00
} ) . toList ( ) ;
final lelantusEntries = await Future . wait ( waitLelantusEntries ) ;
if ( lelantusEntries . isNotEmpty ) {
2023-07-25 20:56:31 +00:00
// should be redundant as _getUnspentCoins() should
// already remove all where value=0
2022-08-26 08:11:35 +00:00
lelantusEntries . removeWhere ( ( element ) = > element . amount = = 0 ) ;
}
return lelantusEntries ;
}
2023-07-24 18:30:01 +00:00
Future < List < isar_models . LelantusCoin > > _getUnspentCoins ( ) async {
final lelantusCoinsList = await db . isar . lelantusCoins
. where ( )
. walletIdEqualTo ( walletId )
. filter ( )
2023-07-24 22:54:38 +00:00
. isUsedEqualTo ( false )
2023-07-24 18:30:01 +00:00
. not ( )
2023-07-24 22:54:38 +00:00
. group ( ( q ) = > q
. valueEqualTo ( " 0 " )
. or ( )
. anonymitySetIdEqualTo ( ANONYMITY_SET_EMPTY_ID ) )
2023-07-24 18:30:01 +00:00
. findAll ( ) ;
2022-08-26 08:11:35 +00:00
2023-07-24 22:49:11 +00:00
return lelantusCoinsList ;
2022-08-26 08:11:35 +00:00
}
// index 0 and 1 for the funds available to spend.
// index 2 and 3 for all the funds in the wallet (including the undependable ones)
2023-01-12 00:16:17 +00:00
// Future<List<Decimal>> _refreshBalance() async {
Future < void > _refreshBalance ( ) async {
2022-08-26 08:11:35 +00:00
try {
2023-01-12 00:16:17 +00:00
final utxosUpdateFuture = _refreshUTXOs ( ) ;
2023-07-24 18:30:01 +00:00
final lelantusCoins = await db . isar . lelantusCoins
. where ( )
. walletIdEqualTo ( walletId )
. filter ( )
2023-07-24 22:49:11 +00:00
. isUsedEqualTo ( false )
2023-07-24 18:30:01 +00:00
. not ( )
. valueEqualTo ( 0. toString ( ) )
. findAll ( ) ;
2023-05-16 21:15:08 +00:00
2023-01-12 00:16:17 +00:00
final currentChainHeight = await chainHeight ;
2022-08-26 08:11:35 +00:00
int intLelantusBalance = 0 ;
int unconfirmedLelantusBalance = 0 ;
2022-09-06 15:34:39 +00:00
2023-07-24 18:30:01 +00:00
for ( final lelantusCoin in lelantusCoins ) {
isar_models . Transaction ? txn = db . isar . transactions
. where ( )
. txidWalletIdEqualTo (
lelantusCoin . txid ,
walletId ,
)
. findFirstSync ( ) ;
if ( txn = = null ) {
Logging . instance . log (
" Transaction not found in DB for lelantus coin: $ lelantusCoin " ,
level: LogLevel . Fatal ,
) ;
} else {
2023-07-24 22:49:11 +00:00
if ( txn . isLelantus ! = true ) {
Logging . instance . log (
" Bad database state found in $ walletName $ walletId for _refreshBalance lelantus " ,
level: LogLevel . Fatal ,
) ;
}
if ( txn . isConfirmed ( currentChainHeight , MINIMUM_CONFIRMATIONS ) ) {
// mint tx, add value to balance
2023-07-24 18:30:01 +00:00
intLelantusBalance + = int . parse ( lelantusCoin . value ) ;
2023-07-24 22:49:11 +00:00
} else {
2023-07-24 18:30:01 +00:00
unconfirmedLelantusBalance + = int . parse ( lelantusCoin . value ) ;
2022-09-06 15:34:39 +00:00
}
2023-07-24 18:30:01 +00:00
}
2022-08-26 08:11:35 +00:00
}
2022-09-06 15:34:39 +00:00
2023-01-12 00:16:17 +00:00
_balancePrivate = Balance (
2023-04-05 22:06:31 +00:00
total: Amount (
rawValue:
BigInt . from ( intLelantusBalance + unconfirmedLelantusBalance ) ,
fractionDigits: coin . decimals ,
) ,
spendable: Amount (
rawValue: BigInt . from ( intLelantusBalance ) ,
fractionDigits: coin . decimals ,
) ,
blockedTotal: Amount (
rawValue: BigInt . zero ,
fractionDigits: coin . decimals ,
) ,
pendingSpendable: Amount (
rawValue: BigInt . from ( unconfirmedLelantusBalance ) ,
fractionDigits: coin . decimals ,
) ,
2023-01-12 00:16:17 +00:00
) ;
2023-01-12 21:20:57 +00:00
await updateCachedBalanceSecondary ( _balancePrivate ! ) ;
2023-03-28 14:10:29 +00:00
// wait for updated uxtos to get updated public balance
await utxosUpdateFuture ;
2022-08-26 08:11:35 +00:00
} catch ( e , s ) {
Logging . instance . log ( " Exception rethrown in getFullBalance(): $ e \n $ s " ,
level: LogLevel . Error ) ;
rethrow ;
}
}
2022-09-06 21:55:01 +00:00
Future < void > anonymizeAllPublicFunds ( ) async {
try {
var mintResult = await _mintSelection ( ) ;
if ( mintResult . isEmpty ) {
Logging . instance . log ( " nothing to mint " , level: LogLevel . Info ) ;
return ;
}
await _submitLelantusToNetwork ( mintResult ) ;
unawaited ( refresh ( ) ) ;
} catch ( e , s ) {
Logging . instance . log (
" Exception caught in anonymizeAllPublicFunds(): $ e \n $ s " ,
level: LogLevel . Warning ) ;
rethrow ;
}
}
/// Returns the mint transaction hex to mint all of the available funds.
Future < Map < String , dynamic > > _mintSelection ( ) async {
2023-01-12 00:16:17 +00:00
final currentChainHeight = await chainHeight ;
final List < isar_models . UTXO > availableOutputs = await utxos ;
final List < isar_models . UTXO ? > spendableOutputs = [ ] ;
2022-09-06 21:55:01 +00:00
// Build list of spendable outputs and totaling their satoshi amount
for ( var i = 0 ; i < availableOutputs . length ; i + + ) {
2023-01-12 00:16:17 +00:00
if ( availableOutputs [ i ] . isBlocked = = false & &
availableOutputs [ i ]
. isConfirmed ( currentChainHeight , MINIMUM_CONFIRMATIONS ) = =
true & &
2022-09-06 21:55:01 +00:00
! ( availableOutputs [ i ] . isCoinbase & &
2023-01-12 00:16:17 +00:00
availableOutputs [ i ] . getConfirmations ( currentChainHeight ) < =
101 ) ) {
2022-09-06 21:55:01 +00:00
spendableOutputs . add ( availableOutputs [ i ] ) ;
}
}
2023-07-24 18:30:01 +00:00
final lelantusCoins = await db . isar . lelantusCoins
. where ( )
. walletIdEqualTo ( walletId )
. filter ( )
. not ( )
. valueEqualTo ( 0. toString ( ) )
. findAll ( ) ;
2022-09-06 21:55:01 +00:00
final data = await _txnData ;
2023-01-12 00:16:17 +00:00
for ( final value in data ) {
2022-09-06 21:55:01 +00:00
if ( value . inputs . isNotEmpty ) {
for ( var element in value . inputs ) {
2023-07-24 18:30:01 +00:00
if ( lelantusCoins . any ( ( e ) = > e . txid = = value . txid ) & &
2022-09-06 21:55:01 +00:00
spendableOutputs . firstWhere (
( output ) = > output ? . txid = = element . txid ,
orElse: ( ) = > null ) ! =
null ) {
spendableOutputs
. removeWhere ( ( output ) = > output ! . txid = = element . txid ) ;
}
}
}
2023-01-12 00:16:17 +00:00
}
2022-09-06 21:55:01 +00:00
// If there is no Utxos to mint then stop the function.
if ( spendableOutputs . isEmpty ) {
Logging . instance . log ( " _mintSelection(): No spendable outputs found " ,
level: LogLevel . Info ) ;
return { } ;
}
int satoshisBeingUsed = 0 ;
2023-01-12 00:16:17 +00:00
List < isar_models . UTXO > utxoObjectsToUse = [ ] ;
2022-09-06 21:55:01 +00:00
for ( var i = 0 ; i < spendableOutputs . length ; i + + ) {
final spendable = spendableOutputs [ i ] ;
if ( spendable ! = null ) {
utxoObjectsToUse . add ( spendable ) ;
satoshisBeingUsed + = spendable . value ;
}
}
var mintsWithoutFee = await createMintsFromAmount ( satoshisBeingUsed ) ;
var tmpTx = await buildMintTransaction (
utxoObjectsToUse , satoshisBeingUsed , mintsWithoutFee ) ;
2023-01-12 00:16:17 +00:00
int vSize = ( tmpTx [ ' transaction ' ] as Transaction ) . virtualSize ( ) ;
final Decimal dvSize = Decimal . fromInt ( vSize ) ;
2022-09-06 21:55:01 +00:00
final feesObject = await fees ;
2023-04-05 22:06:31 +00:00
final Decimal fastFee = Amount (
rawValue: BigInt . from ( feesObject . fast ) ,
fractionDigits: coin . decimals ,
) . decimal ;
2022-09-06 21:55:01 +00:00
int firoFee =
2023-01-12 00:16:17 +00:00
( dvSize * fastFee * Decimal . fromInt ( 100000 ) ) . toDouble ( ) . ceil ( ) ;
// int firoFee = (vSize * feesObject.fast * (1 / 1000.0) * 100000000).ceil();
2022-09-06 21:55:01 +00:00
2023-01-12 00:16:17 +00:00
if ( firoFee < vSize ) {
firoFee = vSize + 1 ;
2022-09-06 21:55:01 +00:00
}
firoFee = firoFee + 10 ;
int satoshiAmountToSend = satoshisBeingUsed - firoFee ;
var mintsWithFee = await createMintsFromAmount ( satoshiAmountToSend ) ;
Map < String , dynamic > transaction = await buildMintTransaction (
utxoObjectsToUse , satoshiAmountToSend , mintsWithFee ) ;
transaction [ ' transaction ' ] = " " ;
Logging . instance . log ( transaction . toString ( ) , level: LogLevel . Info ) ;
Logging . instance . log ( transaction [ ' txHex ' ] , level: LogLevel . Info ) ;
return transaction ;
}
2022-08-26 08:11:35 +00:00
Future < List < Map < String , dynamic > > > createMintsFromAmount ( int total ) async {
2023-07-24 22:49:11 +00:00
int tmpTotal = total ;
2023-07-25 14:55:50 +00:00
int counter = 0 ;
2023-07-24 22:49:11 +00:00
final lastUsedIndex = await db . isar . lelantusCoins
. where ( )
. walletIdEqualTo ( walletId )
. sortByMintIndexDesc ( )
. mintIndexProperty ( )
. findFirst ( ) ;
final nextFreeMintIndex = ( lastUsedIndex ? ? 0 ) + 1 ;
2023-07-25 14:55:50 +00:00
final root = await Bip32Utils . getBip32Root (
( await mnemonic ) . join ( " " ) ,
( await mnemonicPassphrase ) ! ,
_network ,
) ;
final mints = < Map < String , dynamic > > [ ] ;
2022-08-26 08:11:35 +00:00
while ( tmpTotal > 0 ) {
2023-07-25 14:55:50 +00:00
final index = nextFreeMintIndex + counter ;
final bip32 . BIP32 mintKeyPair = await Bip32Utils . getBip32NodeFromRoot (
root ,
constructDerivePath (
networkWIF: _network . wif ,
chain: MINT_INDEX ,
index: index ,
) ,
) ;
final String mintTag = CreateTag (
Format . uint8listToString ( mintKeyPair . privateKey ! ) ,
index ,
Format . uint8listToString ( mintKeyPair . identifier ) ,
isTestnet: coin = = Coin . firoTestNet ,
2022-08-26 08:11:35 +00:00
) ;
2023-07-25 14:55:50 +00:00
final List < Map < String , dynamic > > anonymitySets ;
try {
anonymitySets = await fetchAnonymitySets ( ) ;
} catch ( e , s ) {
Logging . instance . log (
" Firo needs better internet to create mints: $ e \n $ s " ,
level: LogLevel . Fatal ,
) ;
rethrow ;
}
bool isUsedMintTag = false ;
// stupid dynamic maps
for ( final set in anonymitySets ) {
final setCoins = set [ " coins " ] as List ;
for ( final coin in setCoins ) {
if ( coin [ 1 ] = = mintTag ) {
isUsedMintTag = true ;
break ;
}
}
if ( isUsedMintTag ) {
break ;
}
}
if ( isUsedMintTag ) {
Logging . instance . log (
" Found used index when minting " ,
level: LogLevel . Warning ,
) ;
}
if ( ! isUsedMintTag ) {
2023-07-25 15:03:26 +00:00
final mintValue = min ( tmpTotal ,
( coin = = Coin . firoTestNet ? MINT_LIMIT_TESTNET : MINT_LIMIT ) ) ;
2023-07-25 14:55:50 +00:00
final mint = await _getMintHex (
mintValue ,
index ,
) ;
2023-07-25 18:16:08 +00:00
// TODO publicCoin prob shouldn't be empty?
2023-07-25 14:55:50 +00:00
mints . add ( {
" value " : mintValue ,
" script " : mint ,
" index " : index ,
" publicCoin " : " " ,
} ) ;
2023-07-25 15:03:26 +00:00
tmpTotal = tmpTotal -
( coin = = Coin . firoTestNet ? MINT_LIMIT_TESTNET : MINT_LIMIT ) ;
2023-07-25 14:55:50 +00:00
}
counter + + ;
2022-08-26 08:11:35 +00:00
}
return mints ;
}
/// returns a valid txid if successful
Future < String > submitHexToNetwork ( String hex ) async {
try {
final txid = await electrumXClient . broadcastTransaction ( rawTx: hex ) ;
return txid ;
} catch ( e , s ) {
Logging . instance . log (
" Caught exception in submitHexToNetwork( \" $ hex \" ): $ e $ s " ,
printFullLength: true ,
level: LogLevel . Info ) ;
// return an invalid tx
return " transaction submission failed " ;
}
}
/// Builds and signs a transaction
2023-01-12 00:16:17 +00:00
Future < Map < String , dynamic > > buildMintTransaction (
2023-05-16 17:05:25 +00:00
List < isar_models . UTXO > utxosToUse ,
int satoshisPerRecipient ,
List < Map < String , dynamic > > mintsMap ,
) async {
List < String > addressStringsToGet = [ ] ;
2022-08-26 08:11:35 +00:00
// Populating the addresses to derive
for ( var i = 0 ; i < utxosToUse . length ; i + + ) {
final txid = utxosToUse [ i ] . txid ;
final outputIndex = utxosToUse [ i ] . vout ;
// txid may not work for this as txid may not always be the same as tx_hash?
final tx = await cachedElectrumXClient . getTransaction (
txHash: txid ,
verbose: true ,
coin: coin ,
) ;
final vouts = tx [ " vout " ] as List ? ;
if ( vouts ! = null & & outputIndex < vouts . length ) {
final address =
vouts [ outputIndex ] [ " scriptPubKey " ] [ " addresses " ] [ 0 ] as String ? ;
if ( address ! = null ) {
2023-05-16 17:05:25 +00:00
addressStringsToGet . add ( address ) ;
2022-08-26 08:11:35 +00:00
}
}
}
2023-05-16 17:05:25 +00:00
final List < isar_models . Address > addresses = [ ] ;
for ( final addressString in addressStringsToGet ) {
final address = await db . getAddress ( walletId , addressString ) ;
if ( address = = null ) {
Logging . instance . log (
" Failed to fetch the corresponding address object for $ addressString " ,
level: LogLevel . Fatal ,
) ;
} else {
addresses . add ( address ) ;
}
}
List < ECPair > ellipticCurvePairArray = [ ] ;
2022-08-26 08:11:35 +00:00
List < Uint8List > outputDataArray = [ ] ;
2023-05-16 17:05:25 +00:00
Map < String , dynamic > ? receiveDerivations ;
Map < String , dynamic > ? changeDerivations ;
2022-08-26 08:11:35 +00:00
2023-05-16 17:05:25 +00:00
for ( final addressString in addressStringsToGet ) {
String ? pubKey ;
String ? wif ;
2022-08-26 08:11:35 +00:00
2023-05-16 17:05:25 +00:00
final address = await db . getAddress ( walletId , addressString ) ;
2022-08-26 08:11:35 +00:00
2023-05-16 17:05:25 +00:00
if ( address ? . derivationPath ! = null ) {
final node = await Bip32Utils . getBip32Node (
( await mnemonicString ) ! ,
( await mnemonicPassphrase ) ! ,
_network ,
address ! . derivationPath ! . value ,
) ;
wif = node . toWIF ( ) ;
pubKey = Format . uint8listToString ( node . publicKey ) ;
}
2022-08-26 08:11:35 +00:00
2023-05-16 17:05:25 +00:00
if ( wif = = null | | pubKey = = null ) {
receiveDerivations ? ? = Map < String , dynamic > . from (
jsonDecode ( ( await _secureStore . read (
key: " ${ walletId } _receiveDerivations " ) ) ? ?
" {} " ) as Map ,
) ;
for ( var i = 0 ; i < receiveDerivations . length ; i + + ) {
final receive = receiveDerivations [ " $ i " ] ;
if ( receive [ ' address ' ] = = addressString ) {
wif = receive [ ' wif ' ] as String ;
pubKey = receive [ ' publicKey ' ] as String ;
break ;
}
2022-08-26 08:11:35 +00:00
}
2023-05-16 17:05:25 +00:00
if ( wif = = null | | pubKey = = null ) {
changeDerivations ? ? = Map < String , dynamic > . from (
jsonDecode ( ( await _secureStore . read (
key: " ${ walletId } _changeDerivations " ) ) ? ?
" {} " ) as Map ,
) ;
for ( var i = 0 ; i < changeDerivations . length ; i + + ) {
final change = changeDerivations [ " $ i " ] ;
if ( change [ ' address ' ] = = addressString ) {
wif = change [ ' wif ' ] as String ;
pubKey = change [ ' publicKey ' ] as String ;
break ;
}
}
2022-08-26 08:11:35 +00:00
}
}
2023-05-16 17:05:25 +00:00
ellipticCurvePairArray . add (
ECPair . fromWIF (
wif ! ,
network: _network ,
) ,
) ;
outputDataArray . add ( P2PKH (
network: _network ,
data: PaymentData (
pubkey: Format . stringToUint8List (
pubKey ! ,
) ,
) ,
) . data . output ! ) ;
2022-08-26 08:11:35 +00:00
}
final txb = TransactionBuilder ( network: _network ) ;
txb . setVersion ( 2 ) ;
int height = await getBlockHead ( electrumXClient ) ;
txb . setLockTime ( height ) ;
int amount = 0 ;
// Add transaction inputs
for ( var i = 0 ; i < utxosToUse . length ; i + + ) {
txb . addInput (
utxosToUse [ i ] . txid , utxosToUse [ i ] . vout , null , outputDataArray [ i ] ) ;
amount + = utxosToUse [ i ] . value ;
}
for ( var mintsElement in mintsMap ) {
Logging . instance . log ( " using $ mintsElement " , level: LogLevel . Info ) ;
Uint8List mintu8 =
Format . stringToUint8List ( mintsElement [ ' script ' ] as String ) ;
txb . addOutput ( mintu8 , mintsElement [ ' value ' ] as int ) ;
}
for ( var i = 0 ; i < utxosToUse . length ; i + + ) {
txb . sign (
vin: i ,
2023-05-16 17:05:25 +00:00
keyPair: ellipticCurvePairArray [ i ] ,
2022-08-26 08:11:35 +00:00
witnessValue: utxosToUse [ i ] . value ,
) ;
}
var incomplete = txb . buildIncomplete ( ) ;
var txId = incomplete . getId ( ) ;
var txHex = incomplete . toHex ( ) ;
int fee = amount - incomplete . outs [ 0 ] . value ! ;
var builtHex = txb . build ( ) ;
// return builtHex;
2023-01-12 00:16:17 +00:00
// final locale =
// Platform.isWindows ? "en_US" : await Devicelocale.currentLocale;
2022-08-26 08:11:35 +00:00
return {
" transaction " : builtHex ,
" txid " : txId ,
" txHex " : txHex ,
" value " : amount - fee ,
2023-04-05 22:06:31 +00:00
" fees " : Amount (
rawValue: BigInt . from ( fee ) ,
fractionDigits: coin . decimals ,
) . decimal . toDouble ( ) ,
2022-08-26 08:11:35 +00:00
" publicCoin " : " " ,
" height " : height ,
" txType " : " Sent " ,
" confirmed_status " : false ,
2023-04-05 22:06:31 +00:00
" amount " : Amount (
rawValue: BigInt . from ( amount ) ,
fractionDigits: coin . decimals ,
) . decimal . toDouble ( ) ,
2022-08-26 08:11:35 +00:00
" timestamp " : DateTime . now ( ) . millisecondsSinceEpoch ~ / 1000 ,
" subType " : " mint " ,
" mintsMap " : mintsMap ,
} ;
}
2023-01-12 00:16:17 +00:00
Future < void > _refreshLelantusData ( ) async {
2023-07-24 18:30:01 +00:00
final lelantusCoins = await db . isar . lelantusCoins
. where ( )
. walletIdEqualTo ( walletId )
. filter ( )
2023-07-24 22:49:11 +00:00
. isUsedEqualTo ( false )
. and ( )
. isJMintEqualTo ( true )
2023-07-24 18:30:01 +00:00
. not ( )
. valueEqualTo ( 0. toString ( ) )
. findAll ( ) ;
2022-08-26 08:11:35 +00:00
// Get all joinsplit transaction ids
2023-07-24 22:49:11 +00:00
final lelantusJoinSplitTxns = await db
2023-01-17 18:31:07 +00:00
. getTransactions ( walletId )
. filter ( )
. isLelantusEqualTo ( true )
2023-07-24 22:49:11 +00:00
. and ( )
. subTypeEqualTo ( isar_models . TransactionSubType . join )
2023-01-17 18:31:07 +00:00
. findAll ( ) ;
2023-07-24 18:30:01 +00:00
2023-07-24 22:49:11 +00:00
Set < String > joinSplitTXIDs = { } ;
// for (final tx in lelantusJoinSplitTxns) {
// joinSplitTXIDs.add(tx.txid);
// }
2023-07-24 18:30:01 +00:00
for ( final coin in lelantusCoins ) {
2023-07-24 22:49:11 +00:00
joinSplitTXIDs . add ( coin . txid ) ;
2022-08-26 08:11:35 +00:00
}
2023-07-24 22:49:11 +00:00
Map < String , Tuple2 < isar_models . Address ? , isar_models . Transaction > >
updatedData = { } ;
2023-01-17 18:31:07 +00:00
2022-08-26 08:11:35 +00:00
// Grab the most recent information on all the joinsplits
2023-01-12 00:16:17 +00:00
final updatedJSplit = await getJMintTransactions (
cachedElectrumXClient ,
2023-07-24 22:49:11 +00:00
joinSplitTXIDs . toList ( ) ,
2023-01-12 00:16:17 +00:00
coin ,
) ;
final currentChainHeight = await chainHeight ;
2022-08-26 08:11:35 +00:00
// update all of joinsplits that are now confirmed.
2023-01-17 18:31:07 +00:00
for ( final tx in updatedJSplit . entries ) {
2023-01-12 00:16:17 +00:00
isar_models . Transaction ? currentTx ;
try {
2023-01-17 18:31:07 +00:00
currentTx =
2023-07-24 22:49:11 +00:00
lelantusJoinSplitTxns . firstWhere ( ( e ) = > e . txid = = tx . value . txid ) ;
2023-01-12 00:16:17 +00:00
} catch ( _ ) {
currentTx = null ;
}
2022-09-06 15:34:39 +00:00
if ( currentTx = = null ) {
2022-08-26 08:11:35 +00:00
// this send was accidentally not included in the list
2023-01-17 18:31:07 +00:00
tx . value . isLelantus = true ;
2023-07-24 22:49:11 +00:00
updatedData [ tx . value . txid ] =
2023-01-17 18:31:07 +00:00
Tuple2 ( tx . value . address . value ? ? tx . key , tx . value ) ;
2023-07-24 22:49:11 +00:00
} else if ( currentTx . isConfirmed (
currentChainHeight , MINIMUM_CONFIRMATIONS ) ! =
2023-01-17 18:31:07 +00:00
tx . value . isConfirmed ( currentChainHeight , MINIMUM_CONFIRMATIONS ) ) {
tx . value . isLelantus = true ;
2023-07-24 22:49:11 +00:00
updatedData [ tx . value . txid ] =
2023-01-17 18:31:07 +00:00
Tuple2 ( tx . value . address . value ? ? tx . key , tx . value ) ;
2022-08-26 08:11:35 +00:00
}
}
2023-01-12 00:16:17 +00:00
// TODO: optimize this whole lelantus process
2023-01-17 18:31:07 +00:00
2023-02-03 19:22:21 +00:00
final List < Tuple2 < isar_models . Transaction , isar_models . Address ? > > txnsData =
[ ] ;
2023-01-17 18:31:07 +00:00
2023-07-24 22:49:11 +00:00
for ( final value in updatedData . values ) {
2023-01-19 15:16:30 +00:00
// allow possible null address on mints as we don't display address
// this should normally never be null anyways but old (dbVersion up to 4)
// migrated transactions may not have had an address (full rescan should
// fix this)
2023-02-13 22:47:42 +00:00
isar_models . Address ? transactionAddress ;
try {
transactionAddress =
value . item2 . subType = = isar_models . TransactionSubType . mint
? value . item1
: value . item1 ! ;
} catch ( _ ) {
Logging . instance
. log ( " _refreshLelantusData value: $ value " , level: LogLevel . Fatal ) ;
}
2023-01-17 18:31:07 +00:00
final outs =
value . item2 . outputs . where ( ( _ ) = > true ) . toList ( growable: false ) ;
final ins = value . item2 . inputs . where ( ( _ ) = > true ) . toList ( growable: false ) ;
2023-02-03 19:22:21 +00:00
txnsData . add ( Tuple2 (
value . item2 . copyWith ( inputs: ins , outputs: outs ) . item1 ,
transactionAddress ) ) ;
2023-01-17 18:31:07 +00:00
}
2023-01-19 21:13:03 +00:00
await db . addNewTransactionData ( txnsData , walletId ) ;
2022-08-26 08:11:35 +00:00
}
Future < String > _getMintHex ( int amount , int index ) async {
2023-02-03 22:34:06 +00:00
final _mnemonic = await mnemonicString ;
final _mnemonicPassphrase = await mnemonicPassphrase ;
2023-02-13 22:53:28 +00:00
if ( _mnemonicPassphrase = = null ) {
Logging . instance . log (
" Exception in _getMintHex: mnemonic passphrase null, possible migration issue; if using internal builds, delete wallet and restore from seed, if using a release build, please file bug report " ,
level: LogLevel . Error ) ;
}
2023-02-03 22:34:06 +00:00
final derivePath = constructDerivePath (
networkWIF: _network . wif ,
chain: MINT_INDEX ,
index: index ,
2022-08-26 08:11:35 +00:00
) ;
2023-02-03 22:34:06 +00:00
final mintKeyPair = await Bip32Utils . getBip32Node (
_mnemonic ! ,
_mnemonicPassphrase ! ,
_network ,
derivePath ,
) ;
2022-08-26 08:11:35 +00:00
String keydata = Format . uint8listToString ( mintKeyPair . privateKey ! ) ;
String seedID = Format . uint8listToString ( mintKeyPair . identifier ) ;
String mintHex = await compute (
_getMintScriptWrapper ,
Tuple5 (
amount ,
keydata ,
index ,
seedID ,
coin = = Coin . firoTestNet ,
) ,
) ;
return mintHex ;
}
Future < bool > _submitLelantusToNetwork (
Map < String , dynamic > transactionInfo ) async {
2022-09-06 11:47:57 +00:00
final latestSetId = await getLatestSetId ( ) ;
2022-08-26 08:11:35 +00:00
final txid = await submitHexToNetwork ( transactionInfo [ ' txHex ' ] as String ) ;
// success if txid matches the generated txid
Logging . instance . log (
" _submitLelantusToNetwork txid: ${ transactionInfo [ ' txid ' ] } " ,
level: LogLevel . Info ) ;
2023-07-24 18:30:01 +00:00
2022-08-26 08:11:35 +00:00
if ( txid = = transactionInfo [ ' txid ' ] ) {
2023-07-24 22:49:11 +00:00
final lastUsedIndex = await db . isar . lelantusCoins
. where ( )
. walletIdEqualTo ( walletId )
. sortByMintIndexDesc ( )
. mintIndexProperty ( )
. findFirst ( ) ;
final nextFreeMintIndex = ( lastUsedIndex ? ? 0 ) + 1 ;
2022-08-26 08:11:35 +00:00
if ( transactionInfo [ ' spendCoinIndexes ' ] ! = null ) {
// This is a joinsplit
2023-07-24 18:30:01 +00:00
final spentCoinIndexes =
transactionInfo [ ' spendCoinIndexes ' ] as List < int > ;
final List < isar_models . LelantusCoin > updatedCoins = [ ] ;
2022-08-26 08:11:35 +00:00
// Update all of the coins that have been spent.
2023-07-24 18:30:01 +00:00
for ( final index in spentCoinIndexes ) {
final possibleCoins = await db . isar . lelantusCoins
. where ( )
. walletIdEqualTo ( walletId )
. filter ( )
2023-07-24 22:49:11 +00:00
. mintIndexEqualTo ( index )
2023-07-24 18:30:01 +00:00
. findAll ( ) ;
if ( possibleCoins . isNotEmpty ) {
if ( possibleCoins . length > 1 ) {
print (
" ======================= possibleCoins.length > 1 !!! ================================= " ) ;
} else {
final spentCoin = possibleCoins . first ;
updatedCoins . add ( spentCoin . copyWith ( isUsed: true ) ) ;
}
2022-08-26 08:11:35 +00:00
}
}
// if a jmint was made add it to the unspent coin index
2023-07-24 18:30:01 +00:00
final jmint = isar_models . LelantusCoin (
walletId: walletId ,
2023-07-24 21:42:45 +00:00
mintIndex: nextFreeMintIndex ,
2023-07-24 18:30:01 +00:00
value: ( transactionInfo [ ' jmintValue ' ] as int ? ? ? 0 ) . toString ( ) ,
publicCoin: transactionInfo [ ' publicCoin ' ] as String ,
txid: transactionInfo [ ' txid ' ] as String ,
anonymitySetId: latestSetId ,
isUsed: false ,
2023-07-24 21:42:45 +00:00
isJMint: true ,
otherData: null ,
2023-07-24 18:30:01 +00:00
) ;
if ( int . parse ( jmint . value ) > 0 ) {
updatedCoins . add ( jmint ) ;
2022-08-26 08:11:35 +00:00
}
2023-07-24 18:30:01 +00:00
2023-07-24 22:49:11 +00:00
try {
await db . isar . writeTxn ( ( ) async {
for ( final c in updatedCoins ) {
2023-07-25 20:14:07 +00:00
await db . isar . lelantusCoins . deleteByMintIndexWalletId (
c . mintIndex ,
2023-07-24 22:49:11 +00:00
c . walletId ,
) ;
}
await db . isar . lelantusCoins . putAll ( updatedCoins ) ;
} ) ;
} catch ( e , s ) {
Logging . instance . log (
" $ e \n $ s " ,
level: LogLevel . Fatal ,
) ;
rethrow ;
}
2022-08-26 08:11:35 +00:00
2023-04-05 22:06:31 +00:00
final amount = Amount . fromDecimal (
2023-03-24 21:31:05 +00:00
Decimal . parse ( transactionInfo [ " amount " ] . toString ( ) ) ,
2023-04-05 22:06:31 +00:00
fractionDigits: coin . decimals ,
2023-03-24 21:31:05 +00:00
) ;
2022-08-26 08:11:35 +00:00
// add the send transaction
2023-01-16 22:37:00 +00:00
final transaction = isar_models . Transaction (
walletId: walletId ,
txid: transactionInfo [ ' txid ' ] as String ,
timestamp: transactionInfo [ ' timestamp ' ] as int ? ? ?
( DateTime . now ( ) . millisecondsSinceEpoch ~ / 1000 ) ,
2023-07-24 18:30:01 +00:00
type: isar_models . TransactionType . outgoing ,
subType: isar_models . TransactionSubType . join ,
2023-04-05 22:06:31 +00:00
amount: amount . raw . toInt ( ) ,
amountString: amount . toJsonString ( ) ,
fee: Amount . fromDecimal (
2023-01-16 22:37:00 +00:00
Decimal . parse ( transactionInfo [ " fees " ] . toString ( ) ) ,
2023-04-05 22:06:31 +00:00
fractionDigits: coin . decimals ,
) . raw . toInt ( ) ,
2023-01-16 22:37:00 +00:00
height: transactionInfo [ " height " ] as int ? ,
isCancelled: false ,
isLelantus: true ,
slateId: null ,
2023-03-31 16:15:42 +00:00
nonce: null ,
2023-01-16 22:37:00 +00:00
otherData: transactionInfo [ " otherData " ] as String ? ,
2023-02-03 19:22:21 +00:00
inputs: [ ] ,
outputs: [ ] ,
2023-05-28 12:57:05 +00:00
numberOfMessages: null ,
2023-01-16 22:37:00 +00:00
) ;
2023-01-17 18:31:07 +00:00
final transactionAddress = await db
. getAddresses ( walletId )
. filter ( )
. valueEqualTo ( transactionInfo [ " address " ] as String )
. findFirst ( ) ? ?
isar_models . Address (
walletId: walletId ,
value: transactionInfo [ " address " ] as String ,
derivationIndex: - 1 ,
2023-02-03 23:30:32 +00:00
derivationPath: null ,
2023-01-17 18:31:07 +00:00
type: isar_models . AddressType . nonWallet ,
subType: isar_models . AddressSubType . nonWallet ,
publicKey: [ ] ,
) ;
2023-01-12 00:16:17 +00:00
2023-02-03 19:22:21 +00:00
final List < Tuple2 < isar_models . Transaction , isar_models . Address ? > >
txnsData = [ ] ;
2023-01-17 18:31:07 +00:00
2023-02-03 19:22:21 +00:00
txnsData . add ( Tuple2 ( transaction , transactionAddress ) ) ;
2023-01-17 18:31:07 +00:00
2023-01-19 21:13:03 +00:00
await db . addNewTransactionData ( txnsData , walletId ) ;
2022-08-26 08:11:35 +00:00
} else {
// This is a mint
Logging . instance . log ( " this is a mint " , level: LogLevel . Info ) ;
2023-07-24 18:30:01 +00:00
final List < isar_models . LelantusCoin > updatedCoins = [ ] ;
2022-08-26 08:11:35 +00:00
// TODO: transactionInfo['mintsMap']
for ( final mintMap
in transactionInfo [ ' mintsMap ' ] as List < Map < String , dynamic > > ) {
2023-07-24 18:30:01 +00:00
final index = mintMap [ ' index ' ] as int ;
final mint = isar_models . LelantusCoin (
walletId: walletId ,
2023-07-24 21:42:45 +00:00
mintIndex: index ,
2023-07-24 18:30:01 +00:00
value: ( mintMap [ ' value ' ] as int ) . toString ( ) ,
publicCoin: mintMap [ ' publicCoin ' ] as String ,
txid: transactionInfo [ ' txid ' ] as String ,
anonymitySetId: latestSetId ,
isUsed: false ,
2023-07-24 21:42:45 +00:00
isJMint: false ,
otherData: null ,
2022-08-26 08:11:35 +00:00
) ;
2023-07-24 18:30:01 +00:00
if ( int . parse ( mint . value ) > 0 ) {
updatedCoins . add ( mint ) ;
2022-08-26 08:11:35 +00:00
}
}
// Logging.instance.log(coins);
2023-07-24 22:49:11 +00:00
try {
await db . isar . writeTxn ( ( ) async {
await db . isar . lelantusCoins . putAll ( updatedCoins ) ;
} ) ;
} catch ( e , s ) {
Logging . instance . log (
" $ e \n $ s " ,
level: LogLevel . Fatal ,
) ;
rethrow ;
}
2022-08-26 08:11:35 +00:00
}
return true ;
} else {
// Failed to send to network
return false ;
}
}
Future < FeeObject > _getFees ( ) async {
try {
//TODO adjust numbers for different speeds?
const int f = 1 , m = 5 , s = 20 ;
final fast = await electrumXClient . estimateFee ( blocks: f ) ;
final medium = await electrumXClient . estimateFee ( blocks: m ) ;
final slow = await electrumXClient . estimateFee ( blocks: s ) ;
final feeObject = FeeObject (
numberOfBlocksFast: f ,
numberOfBlocksAverage: m ,
numberOfBlocksSlow: s ,
2023-04-05 22:06:31 +00:00
fast: Amount . fromDecimal (
fast ,
fractionDigits: coin . decimals ,
) . raw . toInt ( ) ,
medium: Amount . fromDecimal (
medium ,
fractionDigits: coin . decimals ,
) . raw . toInt ( ) ,
slow: Amount . fromDecimal (
slow ,
fractionDigits: coin . decimals ,
) . raw . toInt ( ) ,
2022-08-26 08:11:35 +00:00
) ;
Logging . instance . log ( " fetched fees: $ feeObject " , level: LogLevel . Info ) ;
return feeObject ;
} catch ( e ) {
Logging . instance
. log ( " Exception rethrown from _getFees(): $ e " , level: LogLevel . Error ) ;
rethrow ;
}
}
Future < ElectrumXNode > _getCurrentNode ( ) async {
2022-11-09 22:43:26 +00:00
final node = NodeService ( secureStorageInterface: _secureStore )
. getPrimaryNodeFor ( coin: coin ) ? ?
2022-08-26 08:11:35 +00:00
DefaultNodes . getNodeFor ( coin ) ;
return ElectrumXNode (
address: node . host ,
port: node . port ,
name: node . name ,
useSSL: node . useSSL ,
id: node . id ,
) ;
}
2023-05-17 14:37:27 +00:00
Future < int > _getTxCount ( { required String address } ) async {
2022-08-26 08:11:35 +00:00
try {
2023-05-17 14:37:27 +00:00
final scriptHash = AddressUtils . convertToScriptHash ( address , _network ) ;
final transactions = await electrumXClient . getHistory (
scripthash: scriptHash ,
) ;
2022-08-26 08:11:35 +00:00
return transactions . length ;
} catch ( e ) {
Logging . instance . log (
2023-05-17 14:37:27 +00:00
" Exception rethrown in _getReceivedTxCount(address: $ address ): $ e " ,
level: LogLevel . Error ,
) ;
2022-08-26 08:11:35 +00:00
rethrow ;
}
}
Future < void > checkReceivingAddressForTransactions ( ) async {
try {
2023-01-12 00:16:17 +00:00
final currentReceiving = await _currentReceivingAddress ;
2023-05-17 14:37:27 +00:00
final int txCount = await _getTxCount ( address: currentReceiving . value ) ;
2022-08-26 08:11:35 +00:00
Logging . instance . log (
2023-01-12 00:16:17 +00:00
' Number of txs for current receiving address $ currentReceiving : $ txCount ' ,
2022-08-26 08:11:35 +00:00
level: LogLevel . Info ) ;
2023-01-23 16:32:53 +00:00
if ( txCount > = 1 | | currentReceiving . derivationIndex < 0 ) {
2023-01-12 00:16:17 +00:00
// First increment the receiving index
final newReceivingIndex = currentReceiving . derivationIndex + 1 ;
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain (
0 ,
newReceivingIndex ,
) ;
2023-01-18 22:55:59 +00:00
final existing = await db
. getAddresses ( walletId )
. filter ( )
. valueEqualTo ( newReceivingAddress . value )
. findFirst ( ) ;
if ( existing = = null ) {
// Add that new change address
await db . putAddress ( newReceivingAddress ) ;
} else {
// we need to update the address
await db . updateAddress ( existing , newReceivingAddress ) ;
}
2023-01-23 16:32:53 +00:00
// keep checking until address with no tx history is set as current
await checkReceivingAddressForTransactions ( ) ;
2022-08-26 08:11:35 +00:00
}
} on SocketException catch ( se , s ) {
Logging . instance . log (
2022-09-07 15:56:10 +00:00
" SocketException caught in checkReceivingAddressForTransactions(): $ se \n $ s " ,
level: LogLevel . Error ) ;
return ;
} catch ( e , s ) {
Logging . instance . log (
" Exception rethrown from checkReceivingAddressForTransactions(): $ e \n $ s " ,
level: LogLevel . Error ) ;
rethrow ;
}
}
Future < void > checkChangeAddressForTransactions ( ) async {
try {
2023-01-12 00:16:17 +00:00
final currentChange = await _currentChangeAddress ;
2023-05-17 14:37:27 +00:00
final int txCount = await _getTxCount ( address: currentChange . value ) ;
2022-09-07 15:56:10 +00:00
Logging . instance . log (
2023-01-12 00:16:17 +00:00
' Number of txs for current change address: $ currentChange : $ txCount ' ,
2022-09-07 15:56:10 +00:00
level: LogLevel . Info ) ;
2023-01-23 16:32:53 +00:00
if ( txCount > = 1 | | currentChange . derivationIndex < 0 ) {
2023-01-12 00:16:17 +00:00
// First increment the change index
final newChangeIndex = currentChange . derivationIndex + 1 ;
// Use new index to derive a new change address
final newChangeAddress = await _generateAddressForChain (
1 ,
newChangeIndex ,
) ;
2023-01-18 22:55:59 +00:00
final existing = await db
. getAddresses ( walletId )
. filter ( )
. valueEqualTo ( newChangeAddress . value )
. findFirst ( ) ;
if ( existing = = null ) {
// Add that new change address
await db . putAddress ( newChangeAddress ) ;
} else {
// we need to update the address
await db . updateAddress ( existing , newChangeAddress ) ;
}
2023-01-23 16:32:53 +00:00
// keep checking until address with no tx history is set as current
await checkChangeAddressForTransactions ( ) ;
2022-09-07 15:56:10 +00:00
}
} on SocketException catch ( se , s ) {
Logging . instance . log (
" SocketException caught in checkChangeAddressForTransactions(): $ se \n $ s " ,
2022-08-26 08:11:35 +00:00
level: LogLevel . Error ) ;
return ;
} catch ( e , s ) {
Logging . instance . log (
2022-09-07 15:56:10 +00:00
" Exception rethrown from checkChangeAddressForTransactions(): $ e \n $ s " ,
2022-08-26 08:11:35 +00:00
level: LogLevel . Error ) ;
rethrow ;
}
}
2023-01-12 00:16:17 +00:00
Future < List < isar_models . Address > > _fetchAllOwnAddresses ( ) async {
2023-01-16 21:04:03 +00:00
final allAddresses = await db
. getAddresses ( walletId )
2023-01-12 00:16:17 +00:00
. filter ( )
2023-01-18 23:20:23 +00:00
. not ( )
2023-05-17 17:34:40 +00:00
. group (
( q ) = > q
. typeEqualTo ( isar_models . AddressType . nonWallet )
. or ( )
. subTypeEqualTo ( isar_models . AddressSubType . nonWallet ) ,
)
2023-01-12 00:16:17 +00:00
. findAll ( ) ;
2022-08-26 08:11:35 +00:00
return allAddresses ;
}
Future < List < Map < String , dynamic > > > _fetchHistory (
List < String > allAddresses ) async {
try {
List < Map < String , dynamic > > allTxHashes = [ ] ;
2022-09-09 15:07:27 +00:00
final Map < int , Map < String , List < dynamic > > > batches = { } ;
final Map < String , String > requestIdToAddressMap = { } ;
const batchSizeMax = 100 ;
int batchNumber = 0 ;
for ( int i = 0 ; i < allAddresses . length ; i + + ) {
if ( batches [ batchNumber ] = = null ) {
batches [ batchNumber ] = { } ;
}
final scripthash =
AddressUtils . convertToScriptHash ( allAddresses [ i ] , _network ) ;
2022-09-12 15:22:05 +00:00
final id = Logger . isTestEnv ? " $ i " : const Uuid ( ) . v1 ( ) ;
2022-09-09 15:07:27 +00:00
requestIdToAddressMap [ id ] = allAddresses [ i ] ;
batches [ batchNumber ] ! . addAll ( {
id: [ scripthash ]
} ) ;
if ( i % batchSizeMax = = batchSizeMax - 1 ) {
batchNumber + + ;
}
}
for ( int i = 0 ; i < batches . length ; i + + ) {
final response =
await _electrumXClient . getBatchHistory ( args: batches [ i ] ! ) ;
for ( final entry in response . entries ) {
for ( int j = 0 ; j < entry . value . length ; j + + ) {
entry . value [ j ] [ " address " ] = requestIdToAddressMap [ entry . key ] ;
if ( ! allTxHashes . contains ( entry . value [ j ] ) ) {
allTxHashes . add ( entry . value [ j ] ) ;
}
2022-08-26 08:11:35 +00:00
}
}
}
return allTxHashes ;
} catch ( e , s ) {
2022-09-09 15:07:27 +00:00
Logging . instance . log ( " _fetchHistory: $ e \n $ s " , level: LogLevel . Error ) ;
rethrow ;
2022-08-26 08:11:35 +00:00
}
}
2023-01-13 21:36:19 +00:00
bool _duplicateTxCheck (
List < Map < String , dynamic > > allTransactions , String txid ) {
for ( int i = 0 ; i < allTransactions . length ; i + + ) {
if ( allTransactions [ i ] [ " txid " ] = = txid ) {
return true ;
}
}
return false ;
}
2023-01-12 00:16:17 +00:00
Future < void > _refreshTransactions ( ) async {
final List < isar_models . Address > allAddresses =
await _fetchAllOwnAddresses ( ) ;
2023-05-23 15:46:34 +00:00
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 ( ) ;
2023-01-12 00:16:17 +00:00
final List < Map < String , dynamic > > allTxHashes =
await _fetchHistory ( allAddresses . map ( ( e ) = > e . value ) . toList ( ) ) ;
2023-01-13 21:36:19 +00:00
List < Map < String , dynamic > > allTransactions = [ ] ;
2022-08-26 08:11:35 +00:00
2023-05-24 16:25:13 +00:00
// final currentHeight = await chainHeight;
2022-08-26 08:11:35 +00:00
2023-01-13 21:36:19 +00:00
for ( final txHash in allTxHashes ) {
2023-05-24 16:25:13 +00:00
// final storedTx = await db
// .getTransactions(walletId)
// .filter()
// .txidEqualTo(txHash["tx_hash"] as String)
// .findFirst();
// if (storedTx == null ||
// !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS)) {
final tx = await cachedElectrumXClient . getTransaction (
txHash: txHash [ " tx_hash " ] as String ,
verbose: true ,
coin: coin ,
) ;
2023-01-13 21:36:19 +00:00
2023-05-24 16:25:13 +00:00
if ( ! _duplicateTxCheck ( allTransactions , tx [ " txid " ] as String ) ) {
tx [ " address " ] = await db
. getAddresses ( walletId )
. filter ( )
. valueEqualTo ( txHash [ " address " ] as String )
. findFirst ( ) ;
tx [ " height " ] = txHash [ " height " ] ;
allTransactions . add ( tx ) ;
2023-01-13 21:36:19 +00:00
}
2023-05-24 16:25:13 +00:00
// }
2023-01-13 21:36:19 +00:00
}
2023-02-03 19:22:21 +00:00
final List < Tuple2 < isar_models . Transaction , isar_models . Address ? > > txnsData =
[ ] ;
2022-08-26 08:11:35 +00:00
2023-01-13 21:36:19 +00:00
for ( final txObject in allTransactions ) {
2023-05-23 15:46:34 +00:00
final inputList = txObject [ " vin " ] as List ;
final outputList = txObject [ " vout " ] as List ;
bool isMint = false ;
bool isJMint = false ;
// 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 ,
) ;
}
2023-01-17 18:31:07 +00:00
}
}
2023-05-23 15:46:34 +00:00
Set < String > inputAddresses = { } ;
Set < String > outputAddresses = { } ;
2023-01-17 18:31:07 +00:00
2023-05-23 15:46:34 +00:00
Amount totalInputValue = Amount (
rawValue: BigInt . zero ,
fractionDigits: coin . decimals ,
) ;
Amount totalOutputValue = Amount (
rawValue: BigInt . zero ,
fractionDigits: coin . decimals ,
) ;
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
2023-01-17 18:31:07 +00:00
final address = input [ " address " ] as String ? ;
final value = input [ " valueSat " ] as int ? ;
2023-05-23 15:46:34 +00:00
// 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
2023-01-17 18:31:07 +00:00
if ( address ! = null & & value ! = null ) {
2023-05-23 15:46:34 +00:00
totalInputValue + = value . toAmountAsRaw (
fractionDigits: coin . decimals ,
) ;
2023-01-17 18:31:07 +00:00
}
2023-05-23 15:46:34 +00:00
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: [ ] ,
2023-05-28 12:57:05 +00:00
numberOfMessages: null ,
2023-05-23 15:46:34 +00:00
) ;
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 ;
2023-01-17 18:31:07 +00:00
}
2023-05-23 15:46:34 +00:00
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 ? ,
) ,
) ;
2023-01-17 18:31:07 +00:00
}
2023-05-23 15:46:34 +00:00
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 ;
2023-01-17 18:31:07 +00:00
final address = output [ " scriptPubKey " ] ? [ " addresses " ] ? [ 0 ] as String ? ? ?
2023-05-23 15:46:34 +00:00
output [ ' scriptPubKey ' ] ? [ ' address ' ] as String ? ;
if ( address ! = null ) {
outputAddresses . add ( address ) ;
if ( receivingAddresses . contains ( address ) | |
changeAddresses . contains ( address ) ) {
amountReceivedInWallet + = value ;
} else {
nonWalletAddressFoundInOutputs = true ;
2023-01-17 18:31:07 +00:00
}
}
2023-05-23 15:46:34 +00:00
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 ( ) ,
) ,
) ;
2023-01-17 18:31:07 +00:00
}
2023-05-23 15:46:34 +00:00
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 ,
2023-05-28 12:57:05 +00:00
numberOfMessages: null ,
2023-05-23 15:46:34 +00:00
) ;
txnsData . add ( Tuple2 ( tx , transactionAddress ) ) ;
// Assume non lelantus transaction =====================================
2023-01-17 18:31:07 +00:00
} else {
2023-05-23 15:46:34 +00:00
// 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 ;
}
2023-01-17 18:31:07 +00:00
}
2023-05-23 15:46:34 +00:00
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 ? ,
) ,
) ;
2023-01-17 18:31:07 +00:00
}
2023-05-23 15:46:34 +00:00
// 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 ;
2023-01-17 18:31:07 +00:00
}
}
2023-05-23 15:46:34 +00:00
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 ( ) ,
) ,
) ;
}
2023-01-17 18:31:07 +00:00
2023-05-23 15:46:34 +00:00
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 ;
2023-01-17 18:31:07 +00:00
}
2023-05-23 15:46:34 +00:00
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 ,
2023-05-28 12:57:05 +00:00
numberOfMessages: null ,
2023-01-17 18:31:07 +00:00
) ;
2023-05-23 15:46:34 +00:00
txnsData . add ( Tuple2 ( tx , transactionAddress ) ) ;
2023-01-17 18:31:07 +00:00
}
2022-08-26 08:11:35 +00:00
}
2023-01-13 21:36:19 +00:00
2023-01-19 21:13:03 +00:00
await db . addNewTransactionData ( txnsData , walletId ) ;
2023-01-19 16:29:00 +00:00
// quick hack to notify manager to call notifyListeners if
// transactions changed
if ( txnsData . isNotEmpty ) {
GlobalEventBus . instance . fire (
UpdatedInBackgroundEvent (
" Transactions updated/added for: $ walletId $ walletName " ,
walletId ,
) ,
) ;
}
2022-08-26 08:11:35 +00:00
}
2023-01-12 00:16:17 +00:00
Future < void > _refreshUTXOs ( ) async {
final allAddresses = await _fetchAllOwnAddresses ( ) ;
2022-08-26 08:11:35 +00:00
try {
2022-09-09 15:07:27 +00:00
final fetchedUtxoList = < List < Map < String , dynamic > > > [ ] ;
2022-08-26 08:11:35 +00:00
2022-09-09 15:07:27 +00:00
final Map < int , Map < String , List < dynamic > > > batches = { } ;
const batchSizeMax = 100 ;
int batchNumber = 0 ;
2022-08-26 08:11:35 +00:00
for ( int i = 0 ; i < allAddresses . length ; i + + ) {
2022-09-09 15:07:27 +00:00
if ( batches [ batchNumber ] = = null ) {
batches [ batchNumber ] = { } ;
}
2022-08-26 08:11:35 +00:00
final scripthash =
2023-01-12 00:16:17 +00:00
AddressUtils . convertToScriptHash ( allAddresses [ i ] . value , _network ) ;
2022-09-09 15:07:27 +00:00
batches [ batchNumber ] ! . addAll ( {
scripthash: [ scripthash ]
} ) ;
if ( i % batchSizeMax = = batchSizeMax - 1 ) {
batchNumber + + ;
2022-08-26 08:11:35 +00:00
}
}
2022-09-09 15:07:27 +00:00
for ( int i = 0 ; i < batches . length ; i + + ) {
final response =
await _electrumXClient . getBatchUTXOs ( args: batches [ i ] ! ) ;
for ( final entry in response . entries ) {
if ( entry . value . isNotEmpty ) {
fetchedUtxoList . add ( entry . value ) ;
}
}
}
2023-01-12 00:16:17 +00:00
final currentChainHeight = await chainHeight ;
final List < isar_models . UTXO > outputArray = [ ] ;
2023-04-05 22:06:31 +00:00
Amount satoshiBalanceTotal = Amount (
rawValue: BigInt . zero ,
fractionDigits: coin . decimals ,
) ;
Amount satoshiBalancePending = Amount (
rawValue: BigInt . zero ,
fractionDigits: coin . decimals ,
) ;
Amount satoshiBalanceSpendable = Amount (
rawValue: BigInt . zero ,
fractionDigits: coin . decimals ,
) ;
Amount satoshiBalanceBlocked = Amount (
rawValue: BigInt . zero ,
fractionDigits: coin . decimals ,
) ;
2022-08-26 08:11:35 +00:00
2022-09-09 15:07:27 +00:00
for ( int i = 0 ; i < fetchedUtxoList . length ; i + + ) {
for ( int j = 0 ; j < fetchedUtxoList [ i ] . length ; j + + ) {
2022-08-26 08:11:35 +00:00
final txn = await cachedElectrumXClient . getTransaction (
2022-09-09 15:07:27 +00:00
txHash: fetchedUtxoList [ i ] [ j ] [ " tx_hash " ] as String ,
2022-08-26 08:11:35 +00:00
verbose: true ,
coin: coin ,
) ;
2023-01-16 22:37:00 +00:00
final utxo = isar_models . UTXO (
walletId: walletId ,
txid: txn [ " txid " ] as String ,
vout: fetchedUtxoList [ i ] [ j ] [ " tx_pos " ] as int ,
value: fetchedUtxoList [ i ] [ j ] [ " value " ] as int ,
name: " " ,
isBlocked: false ,
blockedReason: null ,
isCoinbase: txn [ " is_coinbase " ] as bool ? ? ? false ,
blockHash: txn [ " blockhash " ] as String ? ,
blockHeight: fetchedUtxoList [ i ] [ j ] [ " height " ] as int ? ,
blockTime: txn [ " blocktime " ] as int ? ,
) ;
2023-01-12 00:16:17 +00:00
2023-04-05 22:06:31 +00:00
final utxoAmount = Amount (
rawValue: BigInt . from ( utxo . value ) ,
fractionDigits: coin . decimals ,
) ;
satoshiBalanceTotal = satoshiBalanceTotal + utxoAmount ;
2023-01-12 00:16:17 +00:00
if ( utxo . isBlocked ) {
2023-04-05 22:06:31 +00:00
satoshiBalanceBlocked = satoshiBalanceBlocked + utxoAmount ;
2023-01-12 00:16:17 +00:00
} else {
if ( utxo . isConfirmed ( currentChainHeight , MINIMUM_CONFIRMATIONS ) ) {
2023-04-05 22:06:31 +00:00
satoshiBalanceSpendable = satoshiBalanceSpendable + utxoAmount ;
2023-01-12 00:16:17 +00:00
} else {
2023-04-05 22:06:31 +00:00
satoshiBalancePending = satoshiBalancePending + utxoAmount ;
2023-01-12 00:16:17 +00:00
}
2022-09-06 23:27:14 +00:00
}
2022-08-26 08:11:35 +00:00
2022-09-09 15:07:27 +00:00
outputArray . add ( utxo ) ;
2022-08-26 08:11:35 +00:00
}
}
2023-01-12 00:16:17 +00:00
Logging . instance
. log ( ' Outputs fetched: $ outputArray ' , level: LogLevel . Info ) ;
2022-08-26 08:11:35 +00:00
2023-01-16 21:04:03 +00:00
await db . isar . writeTxn ( ( ) async {
2023-02-01 01:26:23 +00:00
await db . isar . utxos . where ( ) . walletIdEqualTo ( walletId ) . deleteAll ( ) ;
2023-01-16 21:04:03 +00:00
await db . isar . utxos . putAll ( outputArray ) ;
2023-01-12 00:16:17 +00:00
} ) ;
2022-08-26 08:11:35 +00:00
2023-01-12 00:16:17 +00:00
// finally update public balance
_balance = Balance (
total: satoshiBalanceTotal ,
spendable: satoshiBalanceSpendable ,
blockedTotal: satoshiBalanceBlocked ,
pendingSpendable: satoshiBalancePending ,
) ;
2023-01-12 21:20:57 +00:00
await updateCachedBalance ( _balance ! ) ;
2022-09-09 15:07:27 +00:00
} catch ( e , s ) {
Logging . instance
. log ( " Output fetch unsuccessful: $ e \n $ s " , level: LogLevel . Error ) ;
2022-08-26 08:11:35 +00:00
}
}
/// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain]
/// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
Future < String > _getCurrentAddressForChain ( int chain ) async {
2023-01-12 00:16:17 +00:00
final subType = chain = = 0 // Here, we assume that chain == 1 if it isn't 0
? isar_models . AddressSubType . receiving
: isar_models . AddressSubType . change ;
2023-01-16 21:04:03 +00:00
isar_models . Address ? address = await db
. getAddresses ( walletId )
2023-01-12 00:16:17 +00:00
. filter ( )
. typeEqualTo ( isar_models . AddressType . p2pkh )
. subTypeEqualTo ( subType )
. sortByDerivationIndexDesc ( )
. findFirst ( ) ;
return address ! . value ;
2022-08-26 08:11:35 +00:00
}
/// Generates a new internal or external chain address for the wallet using a BIP84 derivation path.
/// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
/// [index] - This can be any integer >= 0
2023-01-12 00:16:17 +00:00
Future < isar_models . Address > _generateAddressForChain (
int chain , int index ) async {
2023-02-03 22:34:06 +00:00
final _mnemonic = await mnemonicString ;
final _mnemonicPassphrase = await mnemonicPassphrase ;
2023-02-13 22:53:28 +00:00
if ( _mnemonicPassphrase = = null ) {
Logging . instance . log (
2023-05-16 17:05:25 +00:00
" Exception in _generateAddressForChain: mnemonic passphrase null, "
" possible migration issue; if using internal builds, delete "
" wallet and restore from seed, if using a release build, "
" please file bug report " ,
2023-02-13 22:53:28 +00:00
level: LogLevel . Error ) ;
}
2023-02-03 23:30:32 +00:00
final derivePath = constructDerivePath (
networkWIF: _network . wif ,
chain: chain ,
index: index ,
) ;
2023-02-03 22:34:06 +00:00
2023-05-16 17:05:25 +00:00
final node = await Bip32Utils . getBip32Node (
_mnemonic ! ,
_mnemonicPassphrase ! ,
_network ,
derivePath ,
) ;
2023-01-12 00:16:17 +00:00
2023-05-16 17:05:25 +00:00
final address = P2PKH (
network: _network ,
data: PaymentData (
pubkey: node . publicKey ,
) ,
) . data . address ! ;
return isar_models . Address (
walletId: walletId ,
value: address ,
publicKey: node . publicKey ,
type: isar_models . AddressType . p2pkh ,
derivationIndex: index ,
derivationPath: isar_models . DerivationPath ( ) . . value = derivePath ,
subType: chain = = 0
? isar_models . AddressSubType . receiving
: isar_models . AddressSubType . change ,
) ;
2022-08-26 08:11:35 +00:00
}
@ override
Future < void > fullRescan (
int maxUnusedAddressGap ,
int maxNumberOfIndexesToCheck ,
) async {
Logging . instance . log ( " Starting full rescan! " , level: LogLevel . Info ) ;
// timer?.cancel();
// for (final isolate in isolates.values) {
// isolate.kill(priority: Isolate.immediate);
// }
// isolates.clear();
longMutex = true ;
GlobalEventBus . instance . fire (
WalletSyncStatusChangedEvent (
WalletSyncStatus . syncing ,
walletId ,
coin ,
) ,
) ;
// clear cache
2022-09-06 15:34:39 +00:00
await _cachedElectrumXClient . clearSharedTransactionCache ( coin: coin ) ;
2022-08-26 08:11:35 +00:00
// back up data
2023-01-12 21:20:57 +00:00
// await _rescanBackup();
2022-08-26 08:11:35 +00:00
2023-01-13 21:47:56 +00:00
// clear blockchain info
2023-01-16 21:04:03 +00:00
await db . deleteWalletBlockchainData ( walletId ) ;
2023-01-17 14:19:30 +00:00
await _deleteDerivations ( ) ;
2023-01-13 21:47:56 +00:00
2022-08-26 08:11:35 +00:00
try {
2023-02-03 22:34:06 +00:00
final _mnemonic = await mnemonicString ;
final _mnemonicPassphrase = await mnemonicPassphrase ;
2023-02-13 22:53:28 +00:00
if ( _mnemonicPassphrase = = null ) {
Logging . instance . log (
" Exception in fullRescan: mnemonic passphrase null, possible migration issue; if using internal builds, delete wallet and restore from seed, if using a release build, please file bug report " ,
level: LogLevel . Error ) ;
}
2023-02-03 22:34:06 +00:00
await _recoverWalletFromBIP32SeedPhrase (
_mnemonic ! ,
_mnemonicPassphrase ! ,
maxUnusedAddressGap ,
2023-05-16 17:05:25 +00:00
maxNumberOfIndexesToCheck ,
true ,
2023-02-03 22:34:06 +00:00
) ;
2022-08-26 08:11:35 +00:00
longMutex = false ;
2023-01-19 16:29:00 +00:00
await refresh ( ) ;
2022-08-26 08:11:35 +00:00
Logging . instance . log ( " Full rescan complete! " , level: LogLevel . Info ) ;
GlobalEventBus . instance . fire (
WalletSyncStatusChangedEvent (
WalletSyncStatus . synced ,
walletId ,
coin ,
) ,
) ;
} catch ( e , s ) {
GlobalEventBus . instance . fire (
WalletSyncStatusChangedEvent (
WalletSyncStatus . unableToSync ,
walletId ,
coin ,
) ,
) ;
// restore from backup
2023-01-12 21:20:57 +00:00
// await _rescanRestore();
2022-08-26 08:11:35 +00:00
longMutex = false ;
Logging . instance . log ( " Exception rethrown from fullRescan(): $ e \n $ s " ,
level: LogLevel . Error ) ;
rethrow ;
}
}
2023-01-17 14:19:30 +00:00
Future < void > _deleteDerivations ( ) async {
// P2PKH derivations
await _secureStore . delete ( key: " ${ walletId } _receiveDerivations " ) ;
await _secureStore . delete ( key: " ${ walletId } _changeDerivations " ) ;
}
2022-08-26 08:11:35 +00:00
/// wrapper for _recoverWalletFromBIP32SeedPhrase()
@ override
Future < void > recoverFromMnemonic ( {
required String mnemonic ,
2023-02-03 22:34:06 +00:00
String ? mnemonicPassphrase ,
2022-08-26 08:11:35 +00:00
required int maxUnusedAddressGap ,
required int maxNumberOfIndexesToCheck ,
required int height ,
} ) async {
try {
await compute (
_setTestnetWrapper ,
coin = = Coin . firoTestNet ,
) ;
Logging . instance . log ( " IS_INTEGRATION_TEST: $ integrationTestFlag " ,
level: LogLevel . Info ) ;
if ( ! integrationTestFlag ) {
final features = await electrumXClient . getServerFeatures ( ) ;
Logging . instance . log ( " features: $ features " , level: LogLevel . Info ) ;
switch ( coin ) {
case Coin . firo:
if ( features [ ' genesis_hash ' ] ! = GENESIS_HASH_MAINNET ) {
throw Exception ( " genesis hash does not match main net! " ) ;
}
break ;
case Coin . firoTestNet:
if ( features [ ' genesis_hash ' ] ! = GENESIS_HASH_TESTNET ) {
throw Exception ( " genesis hash does not match test net! " ) ;
}
break ;
default :
throw Exception (
" Attempted to generate a FiroWallet using a non firo coin type: ${ coin . name } " ) ;
}
// if (_networkType == BasicNetworkType.main) {
// if (features['genesis_hash'] != GENESIS_HASH_MAINNET) {
// throw Exception("genesis hash does not match main net!");
// }
// } else if (_networkType == BasicNetworkType.test) {
// if (features['genesis_hash'] != GENESIS_HASH_TESTNET) {
// throw Exception("genesis hash does not match test net!");
// }
// }
}
// this should never fail
2023-02-03 22:34:06 +00:00
if ( ( await mnemonicString ) ! = null | |
( await this . mnemonicPassphrase ) ! = null ) {
longMutex = false ;
2022-08-26 08:11:35 +00:00
throw Exception ( " Attempted to overwrite mnemonic on restore! " ) ;
}
await _secureStore . write (
key: ' ${ _walletId } _mnemonic ' , value: mnemonic . trim ( ) ) ;
2023-02-03 22:34:06 +00:00
await _secureStore . write (
key: ' ${ _walletId } _mnemonicPassphrase ' ,
value: mnemonicPassphrase ? ? " " ,
) ;
2022-08-26 08:11:35 +00:00
await _recoverWalletFromBIP32SeedPhrase (
2023-02-03 22:34:06 +00:00
mnemonic . trim ( ) ,
mnemonicPassphrase ? ? " " ,
maxUnusedAddressGap ,
2023-05-16 17:05:25 +00:00
maxNumberOfIndexesToCheck ,
false ,
2023-02-03 22:34:06 +00:00
) ;
2022-08-26 08:11:35 +00:00
await compute (
_setTestnetWrapper ,
false ,
) ;
} catch ( e , s ) {
await compute (
_setTestnetWrapper ,
false ,
) ;
Logging . instance . log (
" Exception rethrown from recoverFromMnemonic(): $ e \n $ s " ,
level: LogLevel . Error ) ;
rethrow ;
}
}
bool longMutex = false ;
2022-09-09 15:07:27 +00:00
Future < Map < int , dynamic > > getSetDataMap ( int latestSetId ) async {
final Map < int , dynamic > setDataMap = { } ;
final anonymitySets = await fetchAnonymitySets ( ) ;
for ( int setId = 1 ; setId < = latestSetId ; setId + + ) {
final setData = anonymitySets
. firstWhere ( ( element ) = > element [ " setId " ] = = setId , orElse: ( ) = > { } ) ;
if ( setData . isNotEmpty ) {
setDataMap [ setId ] = setData ;
2022-08-26 08:11:35 +00:00
}
2022-09-09 15:07:27 +00:00
}
return setDataMap ;
}
2022-08-26 08:11:35 +00:00
2023-05-16 17:05:25 +00:00
Future < Map < String , int > > _getBatchTxCount ( {
required Map < String , String > addresses ,
} ) async {
try {
final Map < String , List < dynamic > > args = { } ;
for ( final entry in addresses . entries ) {
args [ entry . key ] = [
AddressUtils . convertToScriptHash ( entry . value , _network )
] ;
}
final response = await electrumXClient . getBatchHistory ( args: args ) ;
final Map < String , int > result = { } ;
for ( final entry in response . entries ) {
result [ entry . key ] = entry . value . length ;
}
return result ;
} catch ( e , s ) {
Logging . instance . log (
" Exception rethrown in _getBatchTxCount(address: $ addresses : $ e \n $ s " ,
level: LogLevel . Error ) ;
rethrow ;
}
}
Future < Tuple2 < List < isar_models . Address > , int > > _checkGaps (
int maxNumberOfIndexesToCheck ,
2023-01-12 00:16:17 +00:00
int maxUnusedAddressGap ,
2023-05-16 17:05:25 +00:00
int txCountBatchSize ,
bip32 . BIP32 root ,
int chain ,
2023-01-12 00:16:17 +00:00
) async {
2023-05-16 17:05:25 +00:00
List < isar_models . Address > addressArray = [ ] ;
int gapCounter = 0 ;
int highestIndexWithHistory = 0 ;
for ( int index = 0 ;
index < maxNumberOfIndexesToCheck & & gapCounter < maxUnusedAddressGap ;
index + = txCountBatchSize ) {
List < String > iterationsAddressArray = [ ] ;
Logging . instance . log (
" index: $ index , \t GapCounter $ chain : $ gapCounter " ,
level: LogLevel . Info ,
) ;
final _id = " k_ $ index " ;
Map < String , String > txCountCallArgs = { } ;
for ( int j = 0 ; j < txCountBatchSize ; j + + ) {
final derivePath = constructDerivePath (
networkWIF: root . network . wif ,
chain: chain ,
index: index + j ,
) ;
final node = await Bip32Utils . getBip32NodeFromRoot ( root , derivePath ) ;
final data = PaymentData ( pubkey: node . publicKey ) ;
final String addressString = P2PKH (
data: data ,
network: _network ,
) . data . address ! ;
const isar_models . AddressType addrType = isar_models . AddressType . p2pkh ;
final address = isar_models . Address (
walletId: walletId ,
value: addressString ,
publicKey: node . publicKey ,
type: addrType ,
derivationIndex: index + j ,
derivationPath: isar_models . DerivationPath ( ) . . value = derivePath ,
subType: chain = = 0
? isar_models . AddressSubType . receiving
: isar_models . AddressSubType . change ,
) ;
addressArray . add ( address ) ;
txCountCallArgs . addAll ( {
" ${ _id } _ $ j " : addressString ,
} ) ;
2022-09-09 15:07:27 +00:00
}
2022-08-26 08:11:35 +00:00
2023-05-16 17:05:25 +00:00
// get address tx counts
final counts = await _getBatchTxCount ( addresses: txCountCallArgs ) ;
// check and add appropriate addresses
for ( int k = 0 ; k < txCountBatchSize ; k + + ) {
int count = counts [ " ${ _id } _ $ k " ] ! ;
if ( count > 0 ) {
iterationsAddressArray . add ( txCountCallArgs [ " ${ _id } _ $ k " ] ! ) ;
// update highest
highestIndexWithHistory = index + k ;
// reset counter
gapCounter = 0 ;
}
2022-08-26 08:11:35 +00:00
2023-05-16 17:05:25 +00:00
// increase counter when no tx history found
if ( count = = 0 ) {
gapCounter + + ;
}
2022-09-09 15:07:27 +00:00
}
2023-05-16 17:05:25 +00:00
// cache all the transactions while waiting for the current function to finish.
unawaited ( getTransactionCacheEarly ( iterationsAddressArray ) ) ;
}
return Tuple2 ( addressArray , highestIndexWithHistory ) ;
}
Future < void > getTransactionCacheEarly ( List < String > allAddresses ) async {
try {
final List < Map < String , dynamic > > allTxHashes =
await _fetchHistory ( allAddresses ) ;
for ( final txHash in allTxHashes ) {
try {
unawaited ( cachedElectrumXClient . getTransaction (
txHash: txHash [ " tx_hash " ] as String ,
verbose: true ,
coin: coin ,
) ) ;
} catch ( e ) {
continue ;
}
2022-09-09 15:07:27 +00:00
}
2023-05-16 17:05:25 +00:00
} catch ( e ) {
//
}
}
Future < void > _recoverHistory (
String suppliedMnemonic ,
String mnemonicPassphrase ,
int maxUnusedAddressGap ,
int maxNumberOfIndexesToCheck ,
bool isRescan ,
) async {
final root = await Bip32Utils . getBip32Root (
suppliedMnemonic ,
mnemonicPassphrase ,
_network ,
) ;
final List < Future < Tuple2 < List < isar_models . Address > , int > > > receiveFutures =
[ ] ;
final List < Future < Tuple2 < List < isar_models . Address > , int > > > changeFutures =
[ ] ;
const receiveChain = 0 ;
const changeChain = 1 ;
const indexZero = 0 ;
// actual size is 36 due to p2pkh, p2sh, and p2wpkh so 12x3
const txCountBatchSize = 12 ;
try {
// receiving addresses
Logging . instance . log (
" checking receiving addresses... " ,
level: LogLevel . Info ,
) ;
receiveFutures . add (
_checkGaps (
maxNumberOfIndexesToCheck ,
maxUnusedAddressGap ,
txCountBatchSize ,
root ,
receiveChain ,
) ,
) ;
// change addresses
Logging . instance . log (
" checking change addresses... " ,
level: LogLevel . Info ,
) ;
changeFutures . add (
_checkGaps (
maxNumberOfIndexesToCheck ,
maxUnusedAddressGap ,
txCountBatchSize ,
root ,
changeChain ,
) ,
) ;
// io limitations may require running these linearly instead
final futuresResult = await Future . wait ( [
Future . wait ( receiveFutures ) ,
Future . wait ( changeFutures ) ,
] ) ;
final receiveResults = futuresResult [ 0 ] ;
final changeResults = futuresResult [ 1 ] ;
final List < isar_models . Address > addressesToStore = [ ] ;
int highestReceivingIndexWithHistory = 0 ;
// If restoring a wallet that never received any funds, then set receivingArray manually
// If we didn't do this, it'd store an empty array
for ( final tuple in receiveResults ) {
if ( tuple . item1 . isEmpty ) {
final address = await _generateAddressForChain (
receiveChain ,
indexZero ,
) ;
addressesToStore . add ( address ) ;
} else {
highestReceivingIndexWithHistory =
max ( tuple . item2 , highestReceivingIndexWithHistory ) ;
addressesToStore . addAll ( tuple . item1 ) ;
2022-08-26 08:11:35 +00:00
}
2022-09-09 15:07:27 +00:00
}
2022-08-26 08:11:35 +00:00
2023-05-16 17:05:25 +00:00
int highestChangeIndexWithHistory = 0 ;
// If restoring a wallet that never sent any funds with change, then set changeArray
// manually. If we didn't do this, it'd store an empty array.
for ( final tuple in changeResults ) {
if ( tuple . item1 . isEmpty ) {
final address = await _generateAddressForChain (
changeChain ,
indexZero ,
) ;
addressesToStore . add ( address ) ;
} else {
highestChangeIndexWithHistory =
max ( tuple . item2 , highestChangeIndexWithHistory ) ;
addressesToStore . addAll ( tuple . item1 ) ;
2022-08-26 08:11:35 +00:00
}
}
2023-05-16 17:05:25 +00:00
// remove extra addresses to help minimize risk of creating a large gap
addressesToStore . removeWhere ( ( e ) = >
e . subType = = isar_models . AddressSubType . change & &
e . derivationIndex > highestChangeIndexWithHistory ) ;
addressesToStore . removeWhere ( ( e ) = >
e . subType = = isar_models . AddressSubType . receiving & &
e . derivationIndex > highestReceivingIndexWithHistory ) ;
2022-08-26 08:11:35 +00:00
2023-05-16 17:05:25 +00:00
if ( isRescan ) {
await db . updateOrPutAddresses ( addressesToStore ) ;
} else {
await db . putAddresses ( addressesToStore ) ;
}
2022-09-09 15:07:27 +00:00
2023-05-16 17:05:25 +00:00
await Future . wait ( [
_refreshTransactions ( ) ,
_refreshUTXOs ( ) ,
] ) ;
await Future . wait ( [
updateCachedId ( walletId ) ,
updateCachedIsFavorite ( false ) ,
] ) ;
longMutex = false ;
} catch ( e , s ) {
Logging . instance . log (
" Exception rethrown from _recoverWalletFromBIP32SeedPhrase(): $ e \n $ s " ,
level: LogLevel . Error ) ;
longMutex = false ;
rethrow ;
}
2022-09-09 15:07:27 +00:00
}
/// Recovers wallet from [suppliedMnemonic]. Expects a valid mnemonic.
Future < void > _recoverWalletFromBIP32SeedPhrase (
2023-02-03 22:34:06 +00:00
String suppliedMnemonic ,
String mnemonicPassphrase ,
int maxUnusedAddressGap ,
2023-05-16 17:05:25 +00:00
int maxNumberOfIndexesToCheck ,
bool isRescan ,
2023-02-03 22:34:06 +00:00
) async {
2022-09-09 15:07:27 +00:00
longMutex = true ;
Logging . instance
. log ( " PROCESSORS ${ Platform . numberOfProcessors } " , level: LogLevel . Info ) ;
try {
final latestSetId = await getLatestSetId ( ) ;
final setDataMap = getSetDataMap ( latestSetId ) ;
2023-05-16 17:05:25 +00:00
2022-09-09 15:07:27 +00:00
final usedSerialNumbers = getUsedCoinSerials ( ) ;
2023-05-16 17:05:25 +00:00
final generateAndCheckAddresses = _recoverHistory (
suppliedMnemonic ,
mnemonicPassphrase ,
maxUnusedAddressGap ,
maxNumberOfIndexesToCheck ,
isRescan ,
) ;
2022-08-26 08:11:35 +00:00
2023-01-12 21:20:57 +00:00
await Future . wait ( [
updateCachedId ( walletId ) ,
updateCachedIsFavorite ( false ) ,
] ) ;
2022-08-26 08:11:35 +00:00
2023-05-16 17:05:25 +00:00
await Future . wait ( [
usedSerialNumbers ,
setDataMap ,
generateAndCheckAddresses ,
] ) ;
2022-09-09 15:07:27 +00:00
await _restore ( latestSetId , await setDataMap , await usedSerialNumbers ) ;
2022-08-26 08:11:35 +00:00
longMutex = false ;
} catch ( e , s ) {
longMutex = false ;
Logging . instance . log (
" Exception rethrown from recoverWalletFromBIP32SeedPhrase(): $ e \n $ s " ,
level: LogLevel . Error ) ;
rethrow ;
}
}
2023-05-16 19:29:48 +00:00
Future < void > _restore (
int latestSetId ,
Map < dynamic , dynamic > setDataMap ,
List < String > usedSerialNumbers ,
) async {
2023-02-03 22:34:06 +00:00
final _mnemonic = await mnemonicString ;
final _mnemonicPassphrase = await mnemonicPassphrase ;
2023-01-17 18:31:07 +00:00
final dataFuture = _refreshTransactions ( ) ;
2022-08-26 08:11:35 +00:00
ReceivePort receivePort = await getIsolate ( {
" function " : " restore " ,
2023-02-03 22:34:06 +00:00
" mnemonic " : _mnemonic ,
" mnemonicPassphrase " : _mnemonicPassphrase ,
2022-08-26 08:11:35 +00:00
" coin " : coin ,
" latestSetId " : latestSetId ,
" setDataMap " : setDataMap ,
" usedSerialNumbers " : usedSerialNumbers ,
" network " : _network ,
2023-07-24 18:30:01 +00:00
" walletId " : walletId ,
2022-08-26 08:11:35 +00:00
} ) ;
2022-09-09 17:14:38 +00:00
await Future . wait ( [ dataFuture ] ) ;
2022-09-09 15:07:27 +00:00
var result = await receivePort . first ;
if ( result is String ) {
2022-08-26 08:11:35 +00:00
Logging . instance
. log ( " restore() ->> this is a string " , level: LogLevel . Error ) ;
stop ( receivePort ) ;
throw Exception ( " isolate restore failed. " ) ;
}
stop ( receivePort ) ;
2022-09-09 15:07:27 +00:00
final message = await staticProcessRestore (
2023-01-17 18:31:07 +00:00
( await _txnData ) ,
2023-01-12 00:16:17 +00:00
result as Map < dynamic , dynamic > ,
await chainHeight ,
) ;
2022-09-09 15:07:27 +00:00
2023-07-24 22:49:11 +00:00
final coins = message [ ' _lelantus_coins ' ] as List < isar_models . LelantusCoin > ;
try {
await db . isar . writeTxn ( ( ) async {
await db . isar . lelantusCoins . putAll ( coins ) ;
} ) ;
} catch ( e , s ) {
Logging . instance . log (
" $ e \n $ s " ,
level: LogLevel . Fatal ,
) ;
2023-07-25 17:17:54 +00:00
// don't just rethrow since isar likes to strip stack traces for some reason
throw Exception ( " e= $ e & s= $ s " ) ;
2023-07-24 22:49:11 +00:00
}
2022-08-26 08:11:35 +00:00
final transactionMap =
2023-01-12 00:16:17 +00:00
message [ " newTxMap " ] as Map < String , isar_models . Transaction > ;
2023-01-17 18:31:07 +00:00
Map < String , Tuple2 < isar_models . Address ? , isar_models . Transaction > > data =
{ } ;
for ( final entry in transactionMap . entries ) {
data [ entry . key ] = Tuple2 ( entry . value . address . value , entry . value ) ;
}
2022-08-26 08:11:35 +00:00
// Create the joinsplit transactions.
final spendTxs = await getJMintTransactions (
2023-01-12 00:16:17 +00:00
_cachedElectrumXClient ,
message [ " spendTxIds " ] as List < String > ,
coin ,
) ;
2022-08-26 08:11:35 +00:00
Logging . instance . log ( spendTxs , level: LogLevel . Info ) ;
2023-01-17 18:31:07 +00:00
for ( var element in spendTxs . entries ) {
final address = element . value . address . value ? ?
data [ element . value . txid ] ? . item1 ? ?
element . key ;
// isar_models.Address(
// walletId: walletId,
// value: transactionInfo["address"] as String,
// derivationIndex: -1,
// type: isar_models.AddressType.nonWallet,
// subType: isar_models.AddressSubType.nonWallet,
// publicKey: [],
// );
data [ element . value . txid ] = Tuple2 ( address , element . value ) ;
2022-08-26 08:11:35 +00:00
}
2023-02-03 19:22:21 +00:00
final List < Tuple2 < isar_models . Transaction , isar_models . Address ? > > txnsData =
[ ] ;
2023-01-17 18:31:07 +00:00
for ( final value in data . values ) {
final transactionAddress = value . item1 ! ;
final outs =
value . item2 . outputs . where ( ( _ ) = > true ) . toList ( growable: false ) ;
final ins = value . item2 . inputs . where ( ( _ ) = > true ) . toList ( growable: false ) ;
2023-02-03 19:22:21 +00:00
txnsData . add ( Tuple2 (
value . item2 . copyWith ( inputs: ins , outputs: outs ) . item1 ,
transactionAddress ) ) ;
2023-01-17 18:31:07 +00:00
}
2023-01-19 21:13:03 +00:00
await db . addNewTransactionData ( txnsData , walletId ) ;
2022-08-26 08:11:35 +00:00
}
Future < List < Map < String , dynamic > > > fetchAnonymitySets ( ) async {
try {
final latestSetId = await getLatestSetId ( ) ;
final List < Map < String , dynamic > > sets = [ ] ;
2022-09-09 17:14:38 +00:00
List < Future < Map < String , dynamic > > > anonFutures = [ ] ;
2022-08-26 08:11:35 +00:00
for ( int i = 1 ; i < = latestSetId ; i + + ) {
2022-09-09 15:07:27 +00:00
final set = cachedElectrumXClient . getAnonymitySet (
2022-08-26 08:11:35 +00:00
groupId: " $ i " ,
coin: coin ,
) ;
2022-09-09 15:07:27 +00:00
anonFutures . add ( set ) ;
}
await Future . wait ( anonFutures ) ;
for ( int i = 1 ; i < = latestSetId ; i + + ) {
2022-09-09 17:14:38 +00:00
Map < String , dynamic > set = ( await anonFutures [ i - 1 ] ) ;
2022-08-26 08:11:35 +00:00
set [ " setId " ] = i ;
sets . add ( set ) ;
}
return sets ;
} catch ( e , s ) {
Logging . instance . log (
" Exception rethrown from refreshAnonymitySets: $ e \n $ s " ,
level: LogLevel . Error ) ;
rethrow ;
}
}
Future < dynamic > _createJoinSplitTransaction (
int spendAmount , String address , bool subtractFeeFromAmount ) async {
2023-02-03 22:34:06 +00:00
final _mnemonic = await mnemonicString ;
final _mnemonicPassphrase = await mnemonicPassphrase ;
2023-07-24 22:49:11 +00:00
final lastUsedIndex = await db . isar . lelantusCoins
. where ( )
. walletIdEqualTo ( walletId )
. sortByMintIndexDesc ( )
. mintIndexProperty ( )
. findFirst ( ) ;
final nextFreeMintIndex = ( lastUsedIndex ? ? 0 ) + 1 ;
2022-08-26 08:11:35 +00:00
final lelantusEntry = await _getLelantusEntry ( ) ;
final anonymitySets = await fetchAnonymitySets ( ) ;
final locktime = await getBlockHead ( electrumXClient ) ;
2023-01-12 00:16:17 +00:00
// final locale =
// Platform.isWindows ? "en_US" : await Devicelocale.currentLocale;
2022-08-26 08:11:35 +00:00
ReceivePort receivePort = await getIsolate ( {
" function " : " createJoinSplit " ,
" spendAmount " : spendAmount ,
" address " : address ,
" subtractFeeFromAmount " : subtractFeeFromAmount ,
2023-02-03 22:34:06 +00:00
" mnemonic " : _mnemonic ,
" mnemonicPassphrase " : _mnemonicPassphrase ,
2023-07-24 22:49:11 +00:00
" index " : nextFreeMintIndex ,
2023-01-12 00:16:17 +00:00
// "price": price,
2022-08-26 08:11:35 +00:00
" lelantusEntries " : lelantusEntry ,
" locktime " : locktime ,
" coin " : coin ,
" network " : _network ,
" _anonymity_sets " : anonymitySets ,
2023-01-12 00:16:17 +00:00
// "locale": locale,
2022-08-26 08:11:35 +00:00
} ) ;
var message = await receivePort . first ;
if ( message is String ) {
Logging . instance
. log ( " Error in CreateJoinSplit: $ message " , level: LogLevel . Error ) ;
stop ( receivePort ) ;
return 3 ;
}
if ( message is int ) {
stop ( receivePort ) ;
return message ;
}
stop ( receivePort ) ;
Logging . instance . log ( ' Closing createJoinSplit! ' , level: LogLevel . Info ) ;
return message ;
}
Future < int > getLatestSetId ( ) async {
try {
final id = await electrumXClient . getLatestCoinId ( ) ;
return id ;
} catch ( e , s ) {
Logging . instance . log ( " Exception rethrown in firo_wallet.dart: $ e \n $ s " ,
level: LogLevel . Error ) ;
rethrow ;
}
}
2023-05-16 19:29:48 +00:00
Future < List < String > > getUsedCoinSerials ( ) async {
2022-08-26 08:11:35 +00:00
try {
final response = await cachedElectrumXClient . getUsedCoinSerials (
coin: coin ,
) ;
return response ;
} catch ( e , s ) {
Logging . instance . log ( " Exception rethrown in firo_wallet.dart: $ e \n $ s " ,
level: LogLevel . Error ) ;
rethrow ;
}
}
@ override
Future < void > exit ( ) async {
_hasCalledExit = true ;
timer ? . cancel ( ) ;
timer = null ;
stopNetworkAlivePinging ( ) ;
for ( final isolate in isolates . values ) {
isolate . kill ( priority: Isolate . immediate ) ;
}
isolates . clear ( ) ;
Logging . instance
. log ( " $ walletName firo_wallet exit finished " , level: LogLevel . Info ) ;
}
bool _hasCalledExit = false ;
@ override
bool get hasCalledExit = > _hasCalledExit ;
bool isActive = false ;
@ override
void Function ( bool ) ? get onIsActiveWalletChanged = > ( isActive ) async {
timer ? . cancel ( ) ;
timer = null ;
if ( isActive ) {
await compute (
_setTestnetWrapper ,
coin = = Coin . firoTestNet ,
) ;
} else {
await compute (
_setTestnetWrapper ,
false ,
) ;
}
this . isActive = isActive ;
} ;
2022-09-06 15:34:39 +00:00
Future < int > estimateJoinSplitFee (
2022-08-26 08:11:35 +00:00
int spendAmount ,
) async {
2022-10-13 19:01:49 +00:00
var lelantusEntry = await _getLelantusEntry ( ) ;
2023-04-05 22:06:31 +00:00
final balance = availablePrivateBalance ( ) . decimal ;
2023-05-24 18:08:32 +00:00
int spendAmount =
( balance * Decimal . fromInt ( Constants . satsPerCoin ( coin ) . toInt ( ) ) )
. toBigInt ( )
. toInt ( ) ;
2022-10-13 19:01:49 +00:00
if ( spendAmount = = 0 | | lelantusEntry . isEmpty ) {
return LelantusFeeData ( 0 , 0 , [ ] ) . fee ;
2022-08-26 08:11:35 +00:00
}
2022-10-13 19:01:49 +00:00
ReceivePort receivePort = await getIsolate ( {
" function " : " estimateJoinSplit " ,
" spendAmount " : spendAmount ,
" subtractFeeFromAmount " : true ,
" lelantusEntries " : lelantusEntry ,
" coin " : coin ,
} ) ;
2022-08-26 08:11:35 +00:00
2022-10-13 19:01:49 +00:00
final message = await receivePort . first ;
if ( message is String ) {
Logging . instance . log ( " this is a string " , level: LogLevel . Error ) ;
stop ( receivePort ) ;
throw Exception ( " _fetchMaxFee isolate failed " ) ;
}
stop ( receivePort ) ;
Logging . instance . log ( ' Closing estimateJoinSplit! ' , level: LogLevel . Info ) ;
return ( message as LelantusFeeData ) . fee ;
2022-08-26 08:11:35 +00:00
}
@ override
2023-04-05 22:06:31 +00:00
Future < Amount > estimateFeeFor ( Amount amount , int feeRate ) async {
int fee = await estimateJoinSplitFee ( amount . raw . toInt ( ) ) ;
return Amount ( rawValue: BigInt . from ( fee ) , fractionDigits: coin . decimals ) ;
2022-08-26 08:11:35 +00:00
}
2023-04-05 22:06:31 +00:00
Future < Amount > estimateFeeForPublic ( Amount amount , int feeRate ) async {
2023-01-12 00:16:17 +00:00
final available = balance . spendable ;
2022-09-07 16:58:54 +00:00
2023-04-05 22:06:31 +00:00
if ( available = = amount ) {
return amount - ( await sweepAllEstimate ( feeRate ) ) ;
} else if ( amount < = Amount . zero | | amount > available ) {
2022-09-07 16:58:54 +00:00
return roughFeeEstimate ( 1 , 2 , feeRate ) ;
}
2023-04-05 22:06:31 +00:00
Amount runningBalance = Amount (
rawValue: BigInt . zero ,
fractionDigits: coin . decimals ,
) ;
2022-09-07 16:58:54 +00:00
int inputCount = 0 ;
2023-01-12 00:16:17 +00:00
for ( final output in ( await utxos ) ) {
if ( ! output . isBlocked ) {
2023-04-05 22:06:31 +00:00
runningBalance = runningBalance +
Amount (
rawValue: BigInt . from ( output . value ) ,
fractionDigits: coin . decimals ,
) ;
2023-01-12 00:16:17 +00:00
inputCount + + ;
2023-04-05 22:06:31 +00:00
if ( runningBalance > amount ) {
2023-01-12 00:16:17 +00:00
break ;
}
2022-09-07 16:58:54 +00:00
}
}
final oneOutPutFee = roughFeeEstimate ( inputCount , 1 , feeRate ) ;
final twoOutPutFee = roughFeeEstimate ( inputCount , 2 , feeRate ) ;
2023-04-05 22:06:31 +00:00
final dustLimitAmount = Amount (
rawValue: BigInt . from ( DUST_LIMIT ) ,
fractionDigits: coin . decimals ,
) ;
if ( runningBalance - amount > oneOutPutFee ) {
if ( runningBalance - amount > oneOutPutFee + dustLimitAmount ) {
final change = runningBalance - amount - twoOutPutFee ;
if ( change > dustLimitAmount & &
runningBalance - amount - change = = twoOutPutFee ) {
return runningBalance - amount - change ;
2022-09-07 16:58:54 +00:00
} else {
2023-04-05 22:06:31 +00:00
return runningBalance - amount ;
2022-09-07 16:58:54 +00:00
}
} else {
2023-04-05 22:06:31 +00:00
return runningBalance - amount ;
2022-09-07 16:58:54 +00:00
}
2023-04-05 22:06:31 +00:00
} else if ( runningBalance - amount = = oneOutPutFee ) {
2022-09-07 16:58:54 +00:00
return oneOutPutFee ;
} else {
return twoOutPutFee ;
}
}
2023-04-05 22:06:31 +00:00
Amount roughFeeEstimate ( int inputCount , int outputCount , int feeRatePerKB ) {
return Amount (
rawValue: BigInt . from ( ( ( 181 * inputCount ) + ( 34 * outputCount ) + 10 ) *
( feeRatePerKB / 1000 ) . ceil ( ) ) ,
fractionDigits: coin . decimals ,
) ;
2022-09-07 16:58:54 +00:00
}
2023-04-05 22:06:31 +00:00
Future < Amount > sweepAllEstimate ( int feeRate ) async {
2022-09-07 16:58:54 +00:00
int available = 0 ;
int inputCount = 0 ;
2023-01-12 00:16:17 +00:00
for ( final output in ( await utxos ) ) {
if ( ! output . isBlocked & &
output . isConfirmed ( storedChainHeight , MINIMUM_CONFIRMATIONS ) ) {
2022-09-07 16:58:54 +00:00
available + = output . value ;
inputCount + + ;
}
}
// transaction will only have 1 output minus the fee
final estimatedFee = roughFeeEstimate ( inputCount , 1 , feeRate ) ;
2023-04-05 22:06:31 +00:00
return Amount (
rawValue: BigInt . from ( available ) ,
fractionDigits: coin . decimals ,
) -
estimatedFee ;
2022-09-07 16:58:54 +00:00
}
2022-09-09 15:07:27 +00:00
Future < List < Map < String , dynamic > > > fastFetch ( List < String > allTxHashes ) async {
List < Map < String , dynamic > > allTransactions = [ ] ;
const futureLimit = 30 ;
List < Future < Map < String , dynamic > > > transactionFutures = [ ] ;
int currentFutureCount = 0 ;
for ( final txHash in allTxHashes ) {
Future < Map < String , dynamic > > transactionFuture =
cachedElectrumXClient . getTransaction (
txHash: txHash ,
verbose: true ,
coin: coin ,
) ;
transactionFutures . add ( transactionFuture ) ;
currentFutureCount + + ;
if ( currentFutureCount > futureLimit ) {
currentFutureCount = 0 ;
await Future . wait ( transactionFutures ) ;
for ( final fTx in transactionFutures ) {
final tx = await fTx ;
// delete unused large parts
tx . remove ( " hex " ) ;
tx . remove ( " lelantusData " ) ;
allTransactions . add ( tx ) ;
}
}
}
if ( currentFutureCount ! = 0 ) {
currentFutureCount = 0 ;
await Future . wait ( transactionFutures ) ;
for ( final fTx in transactionFutures ) {
final tx = await fTx ;
// delete unused large parts
tx . remove ( " hex " ) ;
tx . remove ( " lelantusData " ) ;
allTransactions . add ( tx ) ;
}
}
return allTransactions ;
}
2023-01-17 18:31:07 +00:00
Future < Map < isar_models . Address , isar_models . Transaction > >
getJMintTransactions (
2022-08-26 08:11:35 +00:00
CachedElectrumX cachedClient ,
List < String > transactions ,
2023-01-12 00:16:17 +00:00
// String currency,
2022-08-26 08:11:35 +00:00
Coin coin ,
2023-01-12 00:16:17 +00:00
// Decimal currentPrice,
// String locale,
2022-08-26 08:11:35 +00:00
) async {
try {
2023-01-17 18:31:07 +00:00
Map < isar_models . Address , isar_models . Transaction > txs = { } ;
2022-09-09 15:07:27 +00:00
List < Map < String , dynamic > > allTransactions =
await fastFetch ( transactions ) ;
2022-08-26 08:11:35 +00:00
2022-09-09 15:07:27 +00:00
for ( int i = 0 ; i < allTransactions . length ; i + + ) {
2022-08-26 08:11:35 +00:00
try {
2022-09-09 15:07:27 +00:00
final tx = allTransactions [ i ] ;
2022-08-26 08:11:35 +00:00
var sendIndex = 1 ;
if ( tx [ " vout " ] [ 0 ] [ " value " ] ! = null & &
2022-08-29 23:29:38 +00:00
Decimal . parse ( tx [ " vout " ] [ 0 ] [ " value " ] . toString ( ) ) > Decimal . zero ) {
2022-08-26 08:11:35 +00:00
sendIndex = 0 ;
}
tx [ " amount " ] = tx [ " vout " ] [ sendIndex ] [ " value " ] ;
tx [ " address " ] = tx [ " vout " ] [ sendIndex ] [ " scriptPubKey " ] [ " addresses " ] [ 0 ] ;
tx [ " fees " ] = tx [ " vin " ] [ 0 ] [ " nFees " ] ;
2023-04-05 22:06:31 +00:00
final Amount amount = Amount . fromDecimal (
2023-03-24 21:31:05 +00:00
Decimal . parse ( tx [ " amount " ] . toString ( ) ) ,
2023-04-05 22:06:31 +00:00
fractionDigits: coin . decimals ,
2023-03-24 21:31:05 +00:00
) ;
2023-01-16 22:37:00 +00:00
final txn = isar_models . Transaction (
walletId: walletId ,
txid: tx [ " txid " ] as String ,
timestamp: tx [ " time " ] as int ? ? ?
( DateTime . now ( ) . millisecondsSinceEpoch ~ / 1000 ) ,
type: isar_models . TransactionType . outgoing ,
subType: isar_models . TransactionSubType . join ,
2023-04-05 22:06:31 +00:00
amount: amount . raw . toInt ( ) ,
amountString: amount . toJsonString ( ) ,
fee: Amount . fromDecimal (
2023-01-16 22:37:00 +00:00
Decimal . parse ( tx [ " fees " ] . toString ( ) ) ,
2023-04-05 22:06:31 +00:00
fractionDigits: coin . decimals ,
) . raw . toInt ( ) ,
2023-01-16 22:37:00 +00:00
height: tx [ " height " ] as int ? ,
isCancelled: false ,
isLelantus: true ,
slateId: null ,
otherData: null ,
2023-03-31 16:15:42 +00:00
nonce: null ,
2023-02-03 19:22:21 +00:00
inputs: [ ] ,
outputs: [ ] ,
2023-05-28 12:57:05 +00:00
numberOfMessages: null ,
2023-01-16 22:37:00 +00:00
) ;
2023-01-17 18:31:07 +00:00
final address = await db
. getAddresses ( walletId )
. filter ( )
. valueEqualTo ( tx [ " address " ] as String )
. findFirst ( ) ? ?
isar_models . Address (
walletId: walletId ,
value: tx [ " address " ] as String ,
derivationIndex: - 2 ,
2023-02-03 23:30:32 +00:00
derivationPath: null ,
2023-01-17 18:31:07 +00:00
type: isar_models . AddressType . nonWallet ,
subType: isar_models . AddressSubType . unknown ,
publicKey: [ ] ,
) ;
2023-01-12 00:16:17 +00:00
2023-01-17 18:31:07 +00:00
txs [ address ] = txn ;
2022-08-26 08:11:35 +00:00
} catch ( e , s ) {
Logging . instance . log (
" Exception caught in getJMintTransactions(): $ e \n $ s " ,
level: LogLevel . Info ) ;
rethrow ;
}
}
return txs ;
} catch ( e , s ) {
Logging . instance . log (
" Exception rethrown in getJMintTransactions(): $ e \n $ s " ,
level: LogLevel . Info ) ;
rethrow ;
}
}
2022-09-06 01:18:45 +00:00
@ override
Future < bool > generateNewAddress ( ) async {
try {
2023-01-12 00:16:17 +00:00
final currentReceiving = await _currentReceivingAddress ;
final newReceivingIndex = currentReceiving . derivationIndex + 1 ;
// Use new index to derive a new receiving address
final newReceivingAddress = await _generateAddressForChain (
0 ,
newReceivingIndex ,
) ;
// Add that new receiving address
2023-01-16 21:04:03 +00:00
await db . putAddress ( newReceivingAddress ) ;
2022-09-06 01:18:45 +00:00
return true ;
} catch ( e , s ) {
Logging . instance . log (
" Exception rethrown from generateNewAddress(): $ e \n $ s " ,
level: LogLevel . Error ) ;
return false ;
}
}
2022-09-06 21:52:51 +00:00
2023-04-05 22:06:31 +00:00
Amount availablePrivateBalance ( ) {
return balancePrivate . spendable ;
2023-01-12 00:16:17 +00:00
}
2023-04-05 22:06:31 +00:00
Amount availablePublicBalance ( ) {
return balance . spendable ;
2023-01-12 00:16:17 +00:00
}
Future < int > get chainHeight async {
try {
final result = await _electrumXClient . getBlockHeadTip ( ) ;
2023-01-12 21:20:57 +00:00
final height = result [ " height " ] as int ;
await updateCachedChainHeight ( height ) ;
2023-01-30 17:06:28 +00:00
if ( height > storedChainHeight ) {
GlobalEventBus . instance . fire (
UpdatedInBackgroundEvent (
" Updated current chain height in $ walletId $ walletName ! " ,
walletId ,
) ,
) ;
}
2023-01-12 21:20:57 +00:00
return height ;
2023-01-12 00:16:17 +00:00
} catch ( e , s ) {
Logging . instance . log ( " Exception caught in chainHeight: $ e \n $ s " ,
level: LogLevel . Error ) ;
2023-01-12 21:20:57 +00:00
return storedChainHeight ;
2023-01-12 00:16:17 +00:00
}
}
@ override
2023-01-12 21:20:57 +00:00
int get storedChainHeight = > getCachedChainHeight ( ) ;
2023-01-10 23:50:22 +00:00
@ override
2023-01-12 21:20:57 +00:00
Balance get balance = > _balance ? ? = getCachedBalance ( ) ;
2023-01-12 00:16:17 +00:00
Balance ? _balance ;
2023-01-12 21:20:57 +00:00
Balance get balancePrivate = > _balancePrivate ? ? = getCachedBalanceSecondary ( ) ;
2023-01-12 00:16:17 +00:00
Balance ? _balancePrivate ;
@ override
2023-01-16 21:04:03 +00:00
Future < List < isar_models . UTXO > > get utxos = > db . getUTXOs ( walletId ) . findAll ( ) ;
2023-01-12 00:16:17 +00:00
@ override
Future < List < isar_models . Transaction > > get transactions = >
2023-01-16 21:04:03 +00:00
db . getTransactions ( walletId ) . findAll ( ) ;
2023-04-08 00:44:43 +00:00
@ override
Future < String > get xpub async {
final node = await Bip32Utils . getBip32Root (
( await mnemonic ) . join ( " " ) ,
await mnemonicPassphrase ? ? " " ,
_network ,
) ;
return node . neutered ( ) . toBase58 ( ) ;
}
2022-08-26 08:11:35 +00:00
}