2024-01-09 16:56:05 +00:00
import ' dart:async ' ;
import ' dart:convert ' ;
import ' dart:io ' ;
import ' package:decimal/decimal.dart ' ;
import ' package:flutter/foundation.dart ' ;
import ' package:flutter_libepiccash/lib.dart ' as epiccash ;
import ' package:flutter_libepiccash/models/transaction.dart ' as epic_models ;
2023-11-07 16:25:04 +00:00
import ' package:isar/isar.dart ' ;
2024-01-09 16:56:05 +00:00
import ' package:mutex/mutex.dart ' ;
import ' package:stack_wallet_backup/generate_password.dart ' ;
import ' package:stackwallet/models/balance.dart ' ;
import ' package:stackwallet/models/epicbox_config_model.dart ' ;
import ' package:stackwallet/models/isar/models/blockchain_data/address.dart ' ;
import ' package:stackwallet/models/isar/models/blockchain_data/transaction.dart ' ;
import ' package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart ' ;
import ' package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart ' ;
import ' package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart ' ;
import ' package:stackwallet/models/node_model.dart ' ;
2023-11-03 19:46:55 +00:00
import ' package:stackwallet/models/paymint/fee_object_model.dart ' ;
2023-10-30 22:58:15 +00:00
import ' package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart ' ;
2024-01-09 16:56:05 +00:00
import ' package:stackwallet/services/event_bus/events/global/blocks_remaining_event.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/wallet_sync_status_changed_event.dart ' ;
import ' package:stackwallet/services/event_bus/global_event_bus.dart ' ;
2023-11-03 19:46:55 +00:00
import ' package:stackwallet/utilities/amount/amount.dart ' ;
2024-01-09 16:56:05 +00:00
import ' package:stackwallet/utilities/default_epicboxes.dart ' ;
import ' package:stackwallet/utilities/enums/coin_enum.dart ' ;
import ' package:stackwallet/utilities/flutter_secure_storage_interface.dart ' ;
2023-10-30 22:58:15 +00:00
import ' package:stackwallet/utilities/logger.dart ' ;
2024-01-09 16:56:05 +00:00
import ' package:stackwallet/utilities/stack_file_system.dart ' ;
2023-10-30 22:58:15 +00:00
import ' package:stackwallet/utilities/test_epic_box_connection.dart ' ;
2023-11-06 18:26:33 +00:00
import ' package:stackwallet/wallets/crypto_currency/coins/epiccash.dart ' ;
2023-11-15 17:40:43 +00:00
import ' package:stackwallet/wallets/crypto_currency/crypto_currency.dart ' ;
2023-09-18 21:28:31 +00:00
import ' package:stackwallet/wallets/models/tx_data.dart ' ;
2023-11-04 01:18:22 +00:00
import ' package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart ' ;
2024-01-09 16:56:05 +00:00
import ' package:stackwallet/wallets/wallet/supporting/epiccash_wallet_info_extension.dart ' ;
import ' package:websocket_universal/websocket_universal.dart ' ;
2023-09-18 21:28:31 +00:00
2024-01-09 16:56:05 +00:00
//
// refactor of https://github.com/cypherstack/stack_wallet/blob/1d9fb4cd069f22492ece690ac788e05b8f8b1209/lib/services/coins/epiccash/epiccash_wallet.dart
//
2023-09-18 21:28:31 +00:00
class EpiccashWallet extends Bip39Wallet {
2023-11-15 17:40:43 +00:00
EpiccashWallet ( CryptoCurrencyNetwork network ) : super ( Epiccash ( network ) ) ;
2023-09-18 21:28:31 +00:00
2024-01-09 16:56:05 +00:00
final syncMutex = Mutex ( ) ;
NodeModel ? _epicNode ;
Timer ? timer ;
double highestPercent = 0 ;
Future < double > get getSyncPercent async {
int lastScannedBlock = info . epicData ? . lastScannedBlock ? ? 0 ;
final _chainHeight = await chainHeight ;
double restorePercent = lastScannedBlock / _chainHeight ;
GlobalEventBus . instance
. fire ( RefreshPercentChangedEvent ( highestPercent , walletId ) ) ;
if ( restorePercent > highestPercent ) {
highestPercent = restorePercent ;
}
final int blocksRemaining = _chainHeight - lastScannedBlock ;
GlobalEventBus . instance
. fire ( BlocksRemainingEvent ( blocksRemaining , walletId ) ) ;
return restorePercent < 0 ? 0.0 : restorePercent ;
}
Future < void > updateEpicboxConfig ( String host , int port ) async {
String stringConfig = jsonEncode ( {
" epicbox_domain " : host ,
" epicbox_port " : port ,
" epicbox_protocol_unsecure " : false ,
" epicbox_address_index " : 0 ,
} ) ;
await secureStorageInterface . write (
key: ' ${ walletId } _epicboxConfig ' ,
value: stringConfig ,
) ;
// TODO: refresh anything that needs to be refreshed/updated due to epicbox info changed
}
/// returns an empty String on success, error message on failure
Future < String > cancelPendingTransactionAndPost ( String txSlateId ) async {
try {
final String wallet = ( await secureStorageInterface . read (
key: ' ${ walletId } _wallet ' ,
) ) ! ;
final result = await epiccash . LibEpiccash . cancelTransaction (
wallet: wallet ,
transactionId: txSlateId ,
) ;
Logging . instance . log (
" cancel $ txSlateId result: $ result " ,
level: LogLevel . Info ,
) ;
return result ;
} catch ( e , s ) {
Logging . instance . log ( " $ e , $ s " , level: LogLevel . Error ) ;
return e . toString ( ) ;
}
}
Future < EpicBoxConfigModel > getEpicBoxConfig ( ) async {
EpicBoxConfigModel ? _epicBoxConfig ;
// read epicbox config from secure store
String ? storedConfig =
await secureStorageInterface . read ( key: ' ${ walletId } _epicboxConfig ' ) ;
// we should move to storing the primary server model like we do with nodes, and build the config from that (see epic-mobile)
// EpicBoxServerModel? _epicBox = epicBox ??
// DB.instance.get<EpicBoxServerModel>(
// boxName: DB.boxNamePrimaryEpicBox, key: 'primary');
// Logging.instance.log(
// "Read primary Epic Box config: ${jsonEncode(_epicBox)}",
// level: LogLevel.Info);
if ( storedConfig = = null ) {
// if no config stored, use the default epicbox server as config
_epicBoxConfig =
EpicBoxConfigModel . fromServer ( DefaultEpicBoxes . defaultEpicBoxServer ) ;
} else {
// if a config is stored, test it
_epicBoxConfig = EpicBoxConfigModel . fromString (
storedConfig ) ; // fromString handles checking old config formats
}
bool isEpicboxConnected = await _testEpicboxServer (
_epicBoxConfig . host , _epicBoxConfig . port ? ? 443 ) ;
if ( ! isEpicboxConnected ) {
// default Epicbox is not connected, default to Europe
_epicBoxConfig = EpicBoxConfigModel . fromServer ( DefaultEpicBoxes . europe ) ;
// example of selecting another random server from the default list
// alternative servers: copy list of all default EB servers but remove the default default
// List<EpicBoxServerModel> alternativeServers = DefaultEpicBoxes.all;
// alternativeServers.removeWhere((opt) => opt.name == DefaultEpicBoxes.defaultEpicBoxServer.name);
// alternativeServers.shuffle(); // randomize which server is used
// _epicBoxConfig = EpicBoxConfigModel.fromServer(alternativeServers.first);
// TODO test this connection before returning it
}
return _epicBoxConfig ;
}
// ================= Private =================================================
Future < String > _getConfig ( ) async {
if ( _epicNode = = null ) {
await updateNode ( ) ;
}
final NodeModel node = _epicNode ! ;
final String nodeAddress = node . host ;
final int port = node . port ;
final uri = Uri . parse ( nodeAddress ) . replace ( port: port ) ;
final String nodeApiAddress = uri . toString ( ) ;
final walletDir = await _currentWalletDirPath ( ) ;
final Map < String , dynamic > config = { } ;
config [ " wallet_dir " ] = walletDir ;
config [ " check_node_api_http_addr " ] = nodeApiAddress ;
config [ " chain " ] = " mainnet " ;
config [ " account " ] = " default " ;
config [ " api_listen_port " ] = port ;
config [ " api_listen_interface " ] =
nodeApiAddress . replaceFirst ( uri . scheme , " " ) ;
String stringConfig = jsonEncode ( config ) ;
return stringConfig ;
}
Future < String > _currentWalletDirPath ( ) async {
Directory appDir = await StackFileSystem . applicationRootDirectory ( ) ;
final path = " ${ appDir . path } /epiccash " ;
final String name = walletId . trim ( ) ;
return ' $ path / $ name ' ;
}
Future < int > _nativeFee (
int satoshiAmount , {
bool ifErrorEstimateFee = false ,
} ) async {
final wallet = await secureStorageInterface . read ( key: ' ${ walletId } _wallet ' ) ;
try {
final available = info . cachedBalance . spendable . raw . toInt ( ) ;
var transactionFees = await epiccash . LibEpiccash . getTransactionFees (
wallet: wallet ! ,
amount: satoshiAmount ,
minimumConfirmations: cryptoCurrency . minConfirms ,
available: available ,
) ;
int realFee = 0 ;
try {
realFee =
( Decimal . parse ( transactionFees . fee . toString ( ) ) ) . toBigInt ( ) . toInt ( ) ;
} catch ( e , s ) {
//todo: come back to this
debugPrint ( " $ e $ s " ) ;
}
return realFee ;
} catch ( e , s ) {
Logging . instance . log ( " Error getting fees $ e - $ s " , level: LogLevel . Error ) ;
rethrow ;
}
}
Future < void > _startSync ( ) async {
Logging . instance . log ( " request start sync " , level: LogLevel . Info ) ;
final wallet = await secureStorageInterface . read ( key: ' ${ walletId } _wallet ' ) ;
const int refreshFromNode = 1 ;
if ( ! syncMutex . isLocked ) {
await syncMutex . protect ( ( ) async {
// How does getWalletBalances start syncing????
await epiccash . LibEpiccash . getWalletBalances (
wallet: wallet ! ,
refreshFromNode: refreshFromNode ,
minimumConfirmations: 10 ,
) ;
} ) ;
} else {
Logging . instance . log ( " request start sync denied " , level: LogLevel . Info ) ;
}
}
Future <
( {
double awaitingFinalization ,
double pending ,
double spendable ,
double total
} ) > _allWalletBalances ( ) async {
final wallet = await secureStorageInterface . read ( key: ' ${ walletId } _wallet ' ) ;
const refreshFromNode = 0 ;
return await epiccash . LibEpiccash . getWalletBalances (
wallet: wallet ! ,
refreshFromNode: refreshFromNode ,
minimumConfirmations: cryptoCurrency . minConfirms ,
) ;
}
Future < bool > _testEpicboxServer ( String host , int port ) async {
// TODO use an EpicBoxServerModel as the only param
final websocketConnectionUri = ' wss:// $ host : $ port ' ;
const connectionOptions = SocketConnectionOptions (
pingIntervalMs: 3000 ,
timeoutConnectionMs: 4000 ,
/// see ping/pong messages in [logEventStream] stream
skipPingMessages: true ,
/// Set this attribute to `true` if do not need any ping/pong
/// messages and ping measurement. Default is `false`
pingRestrictionForce: true ,
) ;
final IMessageProcessor < String , String > textSocketProcessor =
SocketSimpleTextProcessor ( ) ;
final textSocketHandler = IWebSocketHandler < String , String > . createClient (
websocketConnectionUri ,
textSocketProcessor ,
connectionOptions: connectionOptions ,
) ;
// Listening to server responses:
bool isConnected = true ;
textSocketHandler . incomingMessagesStream . listen ( ( inMsg ) {
Logging . instance . log (
' > webSocket got text message from server: " $ inMsg " '
' [ping: ${ textSocketHandler . pingDelayMs } ] ' ,
level: LogLevel . Info ) ;
} ) ;
// Connecting to server:
final isTextSocketConnected = await textSocketHandler . connect ( ) ;
if ( ! isTextSocketConnected ) {
// ignore: avoid_print
Logging . instance . log (
' Connection to [ $ websocketConnectionUri ] failed for some reason! ' ,
level: LogLevel . Error ) ;
isConnected = false ;
}
return isConnected ;
}
Future < bool > _putSendToAddresses (
( { String slateId , String commitId } ) slateData ,
Map < String , String > txAddressInfo ,
) async {
try {
2024-01-13 17:09:37 +00:00
final slatesToCommits = info . epicData ? . slatesToCommits ? ? { } ;
2024-01-09 16:56:05 +00:00
final from = txAddressInfo [ ' from ' ] ;
final to = txAddressInfo [ ' to ' ] ;
slatesToCommits [ slateData . slateId ] = {
" commitId " : slateData . commitId ,
" from " : from ,
" to " : to ,
} ;
await info . updateExtraEpiccashWalletInfo (
epicData: info . epicData ! . copyWith (
slatesToCommits: slatesToCommits ,
) ,
isar: mainDB . isar ,
) ;
return true ;
} catch ( e , s ) {
Logging . instance
. log ( " ERROR STORING ADDRESS $ e $ s " , level: LogLevel . Error ) ;
return false ;
}
}
Future < int > _getCurrentIndex ( ) async {
try {
final int receivingIndex = info . epicData ! . receivingIndex ;
// TODO: go through pendingarray and processed array and choose the index
// of the last one that has not been processed, or the index after the one most recently processed;
return receivingIndex ;
} catch ( e , s ) {
Logging . instance . log ( " $ e $ s " , level: LogLevel . Error ) ;
return 0 ;
}
}
Future < Address > _generateAndStoreReceivingAddressForIndex (
int index ,
) async {
Address ? address = await getCurrentReceivingAddress ( ) ;
if ( address = = null ) {
final wallet =
await secureStorageInterface . read ( key: ' ${ walletId } _wallet ' ) ;
EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig ( ) ;
final walletAddress = await epiccash . LibEpiccash . getAddressInfo (
wallet: wallet ! ,
index: index ,
epicboxConfig: epicboxConfig . toString ( ) ,
) ;
Logging . instance . log (
" WALLET_ADDRESS_IS $ walletAddress " ,
level: LogLevel . Info ,
) ;
address = Address (
walletId: walletId ,
value: walletAddress ,
derivationIndex: index ,
derivationPath: null ,
type: AddressType . mimbleWimble ,
subType: AddressSubType . receiving ,
publicKey: [ ] , // ??
) ;
await mainDB . updateOrPutAddresses ( [ address ] ) ;
}
return address ;
}
Future < void > _startScans ( ) async {
try {
//First stop the current listener
epiccash . LibEpiccash . stopEpicboxListener ( ) ;
final wallet =
await secureStorageInterface . read ( key: ' ${ walletId } _wallet ' ) ;
// max number of blocks to scan per loop iteration
const scanChunkSize = 10000 ;
// force firing of scan progress event
await getSyncPercent ;
// fetch current chain height and last scanned block (should be the
// restore height if full rescan or a wallet restore)
int chainHeight = await this . chainHeight ;
int lastScannedBlock = info . epicData ! . lastScannedBlock ;
// loop while scanning in chain in chunks (of blocks?)
while ( lastScannedBlock < chainHeight ) {
Logging . instance . log (
" chainHeight: $ chainHeight , lastScannedBlock: $ lastScannedBlock " ,
level: LogLevel . Info ,
) ;
int nextScannedBlock = await epiccash . LibEpiccash . scanOutputs (
wallet: wallet ! ,
startHeight: lastScannedBlock ,
numberOfBlocks: scanChunkSize ,
) ;
// update local cache
await info . updateExtraEpiccashWalletInfo (
epicData: info . epicData ! . copyWith (
lastScannedBlock: nextScannedBlock ,
) ,
isar: mainDB . isar ,
) ;
// force firing of scan progress event
await getSyncPercent ;
// update while loop condition variables
chainHeight = await this . chainHeight ;
lastScannedBlock = nextScannedBlock ;
}
Logging . instance . log (
" _startScans successfully at the tip " ,
level: LogLevel . Info ,
) ;
//Once scanner completes restart listener
await _listenToEpicbox ( ) ;
} catch ( e , s ) {
Logging . instance . log (
" _startScans failed: $ e \n $ s " ,
level: LogLevel . Error ,
) ;
rethrow ;
}
}
Future < void > _listenToEpicbox ( ) async {
Logging . instance . log ( " STARTING WALLET LISTENER .... " , level: LogLevel . Info ) ;
final wallet = await secureStorageInterface . read ( key: ' ${ walletId } _wallet ' ) ;
EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig ( ) ;
epiccash . LibEpiccash . startEpicboxListener (
wallet: wallet ! ,
epicboxConfig: epicboxConfig . toString ( ) ,
) ;
}
2024-01-13 17:09:37 +00:00
// As opposed to fake config?
2024-01-09 16:56:05 +00:00
Future < String > _getRealConfig ( ) async {
String ? config = await secureStorageInterface . read (
key: ' ${ walletId } _config ' ,
) ;
if ( Platform . isIOS ) {
final walletDir = await _currentWalletDirPath ( ) ;
var editConfig = jsonDecode ( config as String ) ;
editConfig [ " wallet_dir " ] = walletDir ;
config = jsonEncode ( editConfig ) ;
}
return config ! ;
}
// TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index
int _calculateRestoreHeightFrom ( { required DateTime date } ) {
int secondsSinceEpoch = date . millisecondsSinceEpoch ~ / 1000 ;
const int epicCashFirstBlock = 1565370278 ;
const double overestimateSecondsPerBlock = 61 ;
int chosenSeconds = secondsSinceEpoch - epicCashFirstBlock ;
int approximateHeight = chosenSeconds ~ / overestimateSecondsPerBlock ;
//todo: check if print needed
// debugPrint(
// "approximate height: $approximateHeight chosen_seconds: $chosenSeconds");
int height = approximateHeight ;
if ( height < 0 ) {
height = 0 ;
}
return height ;
}
// ============== Overrides ==================================================
@ override
int get isarTransactionVersion = > 2 ;
2023-09-18 21:28:31 +00:00
@ override
2023-11-07 16:25:04 +00:00
FilterOperation ? get changeAddressFilterOperation = >
FilterGroup . and ( standardChangeAddressFilters ) ;
@ override
FilterOperation ? get receivingAddressFilterOperation = >
FilterGroup . and ( standardReceivingAddressFilters ) ;
2024-01-16 17:58:16 +00:00
@ override
Future < void > checkSaveInitialReceivingAddress ( ) async {
// epiccash seems ok with nothing here?
}
2023-11-07 16:25:04 +00:00
@ override
2024-01-09 20:43:58 +00:00
Future < void > init ( { bool ? isRestore } ) async {
if ( isRestore ! = true ) {
String ? encodedWallet =
await secureStorageInterface . read ( key: " ${ walletId } _wallet " ) ;
// check if should create a new wallet
if ( encodedWallet = = null ) {
await updateNode ( ) ;
final mnemonicString = await getMnemonic ( ) ;
final String password = generatePassword ( ) ;
final String stringConfig = await _getConfig ( ) ;
final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig ( ) ;
await secureStorageInterface . write (
key: ' ${ walletId } _config ' , value: stringConfig ) ;
await secureStorageInterface . write (
key: ' ${ walletId } _password ' , value: password ) ;
await secureStorageInterface . write (
key: ' ${ walletId } _epicboxConfig ' , value: epicboxConfig . toString ( ) ) ;
String name = walletId ;
await epiccash . LibEpiccash . initializeNewWallet (
config: stringConfig ,
mnemonic: mnemonicString ,
password: password ,
name: name ,
) ;
2024-01-09 16:56:05 +00:00
2024-01-09 20:43:58 +00:00
//Open wallet
encodedWallet = await epiccash . LibEpiccash . openWallet (
config: stringConfig ,
password: password ,
) ;
await secureStorageInterface . write (
key: ' ${ walletId } _wallet ' ,
value: encodedWallet ,
) ;
2024-01-09 16:56:05 +00:00
2024-01-09 20:43:58 +00:00
//Store Epic box address info
await _generateAndStoreReceivingAddressForIndex ( 0 ) ;
// subtract a couple days to ensure we have a buffer for SWB
final bufferedCreateHeight = _calculateRestoreHeightFrom (
date: DateTime . now ( ) . subtract ( const Duration ( days: 2 ) ) ) ;
final epicData = ExtraEpiccashWalletInfo (
receivingIndex: 0 ,
changeIndex: 0 ,
slatesToAddresses: { } ,
slatesToCommits: { } ,
lastScannedBlock: bufferedCreateHeight ,
restoreHeight: bufferedCreateHeight ,
creationHeight: bufferedCreateHeight ,
) ;
2024-01-09 16:56:05 +00:00
2024-01-09 20:43:58 +00:00
await info . updateExtraEpiccashWalletInfo (
epicData: epicData ,
isar: mainDB . isar ,
) ;
} else {
2024-01-12 19:56:09 +00:00
try {
Logging . instance . log (
" initializeExisting() ${ cryptoCurrency . coin . prettyName } wallet " ,
level: LogLevel . Info ) ;
2024-01-09 16:56:05 +00:00
2024-01-12 19:56:09 +00:00
final config = await _getRealConfig ( ) ;
final password =
await secureStorageInterface . read ( key: ' ${ walletId } _password ' ) ;
2024-01-09 16:56:05 +00:00
2024-01-12 19:56:09 +00:00
final walletOpen = await epiccash . LibEpiccash . openWallet (
config: config ,
password: password ! ,
) ;
await secureStorageInterface . write (
key: ' ${ walletId } _wallet ' , value: walletOpen ) ;
2024-01-09 16:56:05 +00:00
2024-01-12 19:56:09 +00:00
await updateNode ( ) ;
} catch ( e , s ) {
// do nothing, still allow user into wallet
Logging . instance . log (
" $ runtimeType init() failed: $ e \n $ s " ,
level: LogLevel . Error ,
) ;
}
2024-01-09 20:43:58 +00:00
}
2024-01-09 16:56:05 +00:00
}
return await super . init ( ) ;
2023-09-18 21:28:31 +00:00
}
@ override
2024-01-09 16:56:05 +00:00
Future < TxData > confirmSend ( { required TxData txData } ) async {
try {
final wallet =
await secureStorageInterface . read ( key: ' ${ walletId } _wallet ' ) ;
final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig ( ) ;
// TODO determine whether it is worth sending change to a change address.
final String receiverAddress = txData . recipients ! . first . address ;
if ( ! receiverAddress . startsWith ( " http:// " ) | |
! receiverAddress . startsWith ( " https:// " ) ) {
bool isEpicboxConnected = await _testEpicboxServer (
epicboxConfig . host , epicboxConfig . port ? ? 443 ) ;
if ( ! isEpicboxConnected ) {
throw Exception ( " Failed to send TX : Unable to reach epicbox server " ) ;
}
}
( { String commitId , String slateId } ) transaction ;
if ( receiverAddress . startsWith ( " http:// " ) | |
receiverAddress . startsWith ( " https:// " ) ) {
transaction = await epiccash . LibEpiccash . txHttpSend (
wallet: wallet ! ,
selectionStrategyIsAll: 0 ,
minimumConfirmations: cryptoCurrency . minConfirms ,
message: txData . noteOnChain ! ,
amount: txData . recipients ! . first . amount . raw . toInt ( ) ,
address: txData . recipients ! . first . address ,
) ;
} else {
transaction = await epiccash . LibEpiccash . createTransaction (
wallet: wallet ! ,
amount: txData . recipients ! . first . amount . raw . toInt ( ) ,
address: txData . recipients ! . first . address ,
secretKeyIndex: 0 ,
epicboxConfig: epicboxConfig . toString ( ) ,
minimumConfirmations: cryptoCurrency . minConfirms ,
note: txData . noteOnChain ! ,
) ;
}
final Map < String , String > txAddressInfo = { } ;
txAddressInfo [ ' from ' ] = ( await getCurrentReceivingAddress ( ) ) ! . value ;
txAddressInfo [ ' to ' ] = txData . recipients ! . first . address ;
await _putSendToAddresses ( transaction , txAddressInfo ) ;
return txData . copyWith (
txid: transaction . slateId ,
) ;
} catch ( e , s ) {
Logging . instance . log (
" Epic cash confirmSend: $ e \n $ s " ,
level: LogLevel . Error ,
) ;
rethrow ;
}
2023-09-18 21:28:31 +00:00
}
@ override
2024-01-09 16:56:05 +00:00
Future < TxData > prepareSend ( { required TxData txData } ) async {
try {
if ( txData . recipients ? . length ! = 1 ) {
throw Exception ( " Epic cash prepare send requires a single recipient! " ) ;
}
( { String address , Amount amount , bool isChange } ) recipient =
txData . recipients ! . first ;
final int realFee = await _nativeFee ( recipient . amount . raw . toInt ( ) ) ;
final feeAmount = Amount (
rawValue: BigInt . from ( realFee ) ,
fractionDigits: cryptoCurrency . fractionDigits ,
) ;
if ( feeAmount > info . cachedBalance . spendable ) {
throw Exception (
" Epic cash prepare send fee is greater than available balance! " ) ;
}
if ( info . cachedBalance . spendable = = recipient . amount ) {
recipient = (
address: recipient . address ,
amount: recipient . amount - feeAmount ,
isChange: recipient . isChange ,
) ;
}
return txData . copyWith (
recipients: [ recipient ] ,
fee: feeAmount ,
) ;
} catch ( e , s ) {
Logging . instance
. log ( " Epic cash prepareSend: $ e \n $ s " , level: LogLevel . Error ) ;
rethrow ;
}
2023-09-18 21:28:31 +00:00
}
@ override
2024-01-09 16:56:05 +00:00
Future < void > recover ( { required bool isRescan } ) async {
try {
await refreshMutex . protect ( ( ) async {
if ( isRescan ) {
// clear blockchain info
await mainDB . deleteWalletBlockchainData ( walletId ) ;
await info . updateExtraEpiccashWalletInfo (
epicData: info . epicData ! . copyWith (
lastScannedBlock: info . epicData ! . restoreHeight ,
) ,
isar: mainDB . isar ,
) ;
2024-01-09 20:43:58 +00:00
unawaited ( _startScans ( ) ) ;
2024-01-09 16:56:05 +00:00
} else {
await updateNode ( ) ;
final String password = generatePassword ( ) ;
final String stringConfig = await _getConfig ( ) ;
final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig ( ) ;
await secureStorageInterface . write (
key: ' ${ walletId } _config ' ,
value: stringConfig ,
) ;
await secureStorageInterface . write (
key: ' ${ walletId } _password ' ,
value: password ,
) ;
await secureStorageInterface . write (
key: ' ${ walletId } _epicboxConfig ' ,
value: epicboxConfig . toString ( ) ,
) ;
await epiccash . LibEpiccash . recoverWallet (
config: stringConfig ,
password: password ,
mnemonic: await getMnemonic ( ) ,
name: info . walletId ,
) ;
final epicData = ExtraEpiccashWalletInfo (
receivingIndex: 0 ,
changeIndex: 0 ,
slatesToAddresses: { } ,
slatesToCommits: { } ,
lastScannedBlock: info . restoreHeight ,
restoreHeight: info . restoreHeight ,
creationHeight: info . epicData ? . creationHeight ? ? info . restoreHeight ,
) ;
await info . updateExtraEpiccashWalletInfo (
epicData: epicData ,
isar: mainDB . isar ,
) ;
//Open Wallet
final walletOpen = await epiccash . LibEpiccash . openWallet (
config: stringConfig ,
password: password ,
) ;
await secureStorageInterface . write (
key: ' ${ walletId } _wallet ' ,
value: walletOpen ,
) ;
await _generateAndStoreReceivingAddressForIndex (
epicData . receivingIndex ) ;
}
} ) ;
2024-01-09 20:43:58 +00:00
unawaited ( refresh ( ) ) ;
2024-01-09 16:56:05 +00:00
} catch ( e , s ) {
Logging . instance . log (
" Exception rethrown from electrumx_mixin recover(): $ e \n $ s " ,
level: LogLevel . Info ) ;
rethrow ;
}
2023-09-18 21:28:31 +00:00
}
@ override
2024-01-09 16:56:05 +00:00
Future < void > refresh ( ) async {
// Awaiting this lock could be dangerous.
// Since refresh is periodic (generally)
if ( refreshMutex . isLocked ) {
return ;
}
try {
// this acquire should be almost instant due to above check.
// Slight possibility of race but should be irrelevant
await refreshMutex . acquire ( ) ;
GlobalEventBus . instance . fire (
WalletSyncStatusChangedEvent (
WalletSyncStatus . syncing ,
walletId ,
cryptoCurrency . coin ,
) ,
) ;
// if (info.epicData?.creationHeight == null) {
// await info.updateExtraEpiccashWalletInfo(epicData: inf, isar: isar)
// await epicUpdateCreationHeight(await chainHeight);
// }
// this will always be zero????
final int curAdd = await _getCurrentIndex ( ) ;
await _generateAndStoreReceivingAddressForIndex ( curAdd ) ;
await _startScans ( ) ;
unawaited ( _startSync ( ) ) ;
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.0 , walletId ) ) ;
await updateChainHeight ( ) ;
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.1 , walletId ) ) ;
// if (this is MultiAddressInterface) {
// await (this as MultiAddressInterface)
// .checkReceivingAddressForTransactions();
// }
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.2 , walletId ) ) ;
// // TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
// if (this is MultiAddressInterface) {
// await (this as MultiAddressInterface)
// .checkChangeAddressForTransactions();
// }
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.3 , walletId ) ) ;
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.50 , walletId ) ) ;
final fetchFuture = updateTransactions ( ) ;
// if (currentHeight != storedHeight) {
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.60 , walletId ) ) ;
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.70 , walletId ) ) ;
await fetchFuture ;
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.80 , walletId ) ) ;
// await getAllTxsToWatch();
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 0.90 , walletId ) ) ;
await updateBalance ( ) ;
GlobalEventBus . instance . fire ( RefreshPercentChangedEvent ( 1.0 , walletId ) ) ;
GlobalEventBus . instance . fire (
WalletSyncStatusChangedEvent (
WalletSyncStatus . synced ,
walletId ,
cryptoCurrency . coin ,
) ,
) ;
if ( shouldAutoSync ) {
timer ? ? = Timer . periodic ( const Duration ( seconds: 150 ) , ( timer ) async {
// chain height check currently broken
// if ((await chainHeight) != (await storedChainHeight)) {
// TODO: [prio=med] some kind of quick check if wallet needs to refresh to replace the old refreshIfThereIsNewData call
// if (await refreshIfThereIsNewData()) {
unawaited ( refresh ( ) ) ;
// }
// }
} ) ;
}
} catch ( error , strace ) {
GlobalEventBus . instance . fire (
NodeConnectionStatusChangedEvent (
NodeConnectionStatus . disconnected ,
walletId ,
cryptoCurrency . coin ,
) ,
) ;
GlobalEventBus . instance . fire (
WalletSyncStatusChangedEvent (
WalletSyncStatus . unableToSync ,
walletId ,
cryptoCurrency . coin ,
) ,
) ;
Logging . instance . log (
" Caught exception in refreshWalletData(): $ error \n $ strace " ,
level: LogLevel . Error ,
) ;
} finally {
refreshMutex . release ( ) ;
}
2023-09-18 21:28:31 +00:00
}
@ override
2024-01-09 16:56:05 +00:00
Future < void > updateBalance ( ) async {
try {
final balances = await _allWalletBalances ( ) ;
final balance = Balance (
total: Amount . fromDecimal (
Decimal . parse ( balances . total . toString ( ) ) +
Decimal . parse ( balances . awaitingFinalization . toString ( ) ) ,
fractionDigits: cryptoCurrency . fractionDigits ,
) ,
spendable: Amount . fromDecimal (
Decimal . parse ( balances . spendable . toString ( ) ) ,
fractionDigits: cryptoCurrency . fractionDigits ,
) ,
blockedTotal: Amount . zeroWith (
fractionDigits: cryptoCurrency . fractionDigits ,
) ,
pendingSpendable: Amount . fromDecimal (
Decimal . parse ( balances . pending . toString ( ) ) ,
fractionDigits: cryptoCurrency . fractionDigits ,
) ,
) ;
await info . updateBalance (
newBalance: balance ,
isar: mainDB . isar ,
) ;
} catch ( e , s ) {
Logging . instance . log (
" Epic cash wallet failed to update balance: $ e \n $ s " ,
level: LogLevel . Warning ,
) ;
}
2023-09-18 21:28:31 +00:00
}
@ override
2024-01-09 16:56:05 +00:00
Future < void > updateTransactions ( ) async {
try {
final wallet =
await secureStorageInterface . read ( key: ' ${ walletId } _wallet ' ) ;
const refreshFromNode = 1 ;
final myAddresses = await mainDB
. getAddresses ( walletId )
. filter ( )
. typeEqualTo ( AddressType . mimbleWimble )
. and ( )
. subTypeEqualTo ( AddressSubType . receiving )
. and ( )
. valueIsNotEmpty ( )
. valueProperty ( )
. findAll ( ) ;
final myAddressesSet = myAddresses . toSet ( ) ;
final transactions = await epiccash . LibEpiccash . getTransactions (
wallet: wallet ! ,
refreshFromNode: refreshFromNode ,
) ;
final List < TransactionV2 > txns = [ ] ;
2024-01-13 17:09:37 +00:00
final slatesToCommits = info . epicData ? . slatesToCommits ? ? { } ;
2024-01-09 16:56:05 +00:00
for ( final tx in transactions ) {
// Logging.instance.log("tx: $tx", level: LogLevel.Info);
2024-01-09 20:43:58 +00:00
final isIncoming =
tx . txType = = epic_models . TransactionType . TxReceived | |
tx . txType = = epic_models . TransactionType . TxReceivedCancelled ;
2024-01-09 16:56:05 +00:00
final slateId = tx . txSlateId ;
final commitId = slatesToCommits [ slateId ] ? [ ' commitId ' ] as String ? ;
final numberOfMessages = tx . messages ? . messages . length ;
final onChainNote = tx . messages ? . messages [ 0 ] . message ;
final addressFrom = slatesToCommits [ slateId ] ? [ " from " ] as String ? ;
final addressTo = slatesToCommits [ slateId ] ? [ " to " ] as String ? ;
2024-01-09 20:43:58 +00:00
final credit = int . parse ( tx . amountCredited ) ;
final debit = int . parse ( tx . amountDebited ) ;
final fee = int . tryParse ( tx . fee ? ? " 0 " ) ? ? 0 ;
2024-01-09 16:56:05 +00:00
// hack epic tx data into inputs and outputs
final List < OutputV2 > outputs = [ ] ;
final List < InputV2 > inputs = [ ] ;
final addressFromIsMine = myAddressesSet . contains ( addressFrom ) ;
final addressToIsMine = myAddressesSet . contains ( addressTo ) ;
2024-01-09 20:43:58 +00:00
OutputV2 output = OutputV2 . isarCantDoRequiredInDefaultConstructor (
scriptPubKeyHex: " 00 " ,
valueStringSats: credit . toString ( ) ,
addresses: [
if ( addressFrom ! = null ) addressFrom ,
] ,
walletOwns: true ,
2024-01-09 16:56:05 +00:00
) ;
2024-01-09 20:43:58 +00:00
InputV2 input = InputV2 . isarCantDoRequiredInDefaultConstructor (
scriptSigHex: null ,
2024-01-14 17:26:04 +00:00
scriptSigAsm: null ,
2024-01-09 20:43:58 +00:00
sequence: null ,
outpoint: null ,
addresses: [ if ( addressTo ! = null ) addressTo ] ,
valueStringSats: debit . toString ( ) ,
witness: null ,
innerRedeemScriptAsm: null ,
coinbase: null ,
walletOwns: true ,
2024-01-09 16:56:05 +00:00
) ;
final TransactionType txType ;
2024-01-09 20:43:58 +00:00
if ( isIncoming ) {
if ( addressToIsMine & & addressFromIsMine ) {
txType = TransactionType . sentToSelf ;
} else {
txType = TransactionType . incoming ;
}
output = output . copyWith (
addresses: [
myAddressesSet
. first , // Must be changed if we ever do more than a single wallet address!!!
] ,
walletOwns: true ,
) ;
2024-01-09 16:56:05 +00:00
} else {
txType = TransactionType . outgoing ;
}
2024-01-09 20:43:58 +00:00
outputs . add ( output ) ;
inputs . add ( input ) ;
2024-01-09 16:56:05 +00:00
final otherData = {
" isEpiccashTransaction " : true ,
" numberOfMessages " : numberOfMessages ,
" slateId " : slateId ,
" onChainNote " : onChainNote ,
" isCancelled " :
tx . txType = = epic_models . TransactionType . TxSentCancelled | |
tx . txType = = epic_models . TransactionType . TxReceivedCancelled ,
2024-01-10 22:28:30 +00:00
" overrideFee " : Amount (
2024-01-09 20:43:58 +00:00
rawValue: BigInt . from ( fee ) ,
fractionDigits: cryptoCurrency . fractionDigits ,
) . toJsonString ( ) ,
2024-01-09 16:56:05 +00:00
} ;
final txn = TransactionV2 (
walletId: walletId ,
blockHash: null ,
hash: commitId ? ? tx . id . toString ( ) ,
txid: commitId ? ? tx . id . toString ( ) ,
timestamp:
DateTime . parse ( tx . creationTs ) . millisecondsSinceEpoch ~ / 1000 ,
height: tx . confirmed ? tx . kernelLookupMinHeight ? ? 1 : null ,
inputs: List . unmodifiable ( inputs ) ,
outputs: List . unmodifiable ( outputs ) ,
version: 0 ,
type: txType ,
subType: TransactionSubType . none ,
otherData: jsonEncode ( otherData ) ,
) ;
txns . add ( txn ) ;
}
await mainDB . updateOrPutTransactionV2s ( txns ) ;
} catch ( e , s ) {
Logging . instance . log (
" ${ cryptoCurrency . runtimeType } ${ cryptoCurrency . network } net wallet "
" \" ${ info . name } \" _ ${ info . walletId } updateTransactions() failed: $ e \n $ s " ,
level: LogLevel . Warning ,
) ;
}
}
@ override
Future < bool > updateUTXOs ( ) async {
// not used for epiccash
return false ;
2023-09-18 21:28:31 +00:00
}
2023-10-30 22:58:15 +00:00
@ override
2024-01-09 16:56:05 +00:00
Future < void > updateNode ( ) async {
_epicNode = getCurrentNode ( ) ;
// TODO: [prio=low] move this out of secure storage if secure storage not needed
final String stringConfig = await _getConfig ( ) ;
await secureStorageInterface . write (
key: ' ${ walletId } _config ' ,
value: stringConfig ,
) ;
2024-01-12 19:56:09 +00:00
// unawaited(refresh());
2023-10-30 22:58:15 +00:00
}
@ override
Future < bool > pingCheck ( ) async {
try {
final node = nodeService . getPrimaryNodeFor ( coin: cryptoCurrency . coin ) ;
// force unwrap optional as we want connection test to fail if wallet
// wasn't initialized or epicbox node was set to null
return await testEpicNodeConnection (
NodeFormData ( )
. . host = node ! . host
. . useSSL = node . useSSL
. . port = node . port ,
) ! =
null ;
} catch ( e , s ) {
Logging . instance . log ( " $ e \n $ s " , level: LogLevel . Info ) ;
return false ;
}
}
2023-10-31 16:06:35 +00:00
@ override
Future < void > updateChainHeight ( ) async {
2024-01-09 16:56:05 +00:00
final config = await _getRealConfig ( ) ;
final latestHeight =
await epiccash . LibEpiccash . getChainHeight ( config: config ) ;
await info . updateCachedChainHeight (
newHeight: latestHeight ,
isar: mainDB . isar ,
) ;
2023-10-31 16:06:35 +00:00
}
2023-11-03 19:46:55 +00:00
@ override
2024-01-09 16:56:05 +00:00
Future < Amount > estimateFeeFor ( Amount amount , int feeRate ) async {
// setting ifErrorEstimateFee doesn't do anything as its not used in the nativeFee function?????
int currentFee = await _nativeFee (
amount . raw . toInt ( ) ,
ifErrorEstimateFee: true ,
) ;
return Amount (
rawValue: BigInt . from ( currentFee ) ,
fractionDigits: cryptoCurrency . fractionDigits ,
) ;
2023-11-03 19:46:55 +00:00
}
@ override
2024-01-09 16:56:05 +00:00
Future < FeeObject > get fees async {
// this wasn't done before the refactor either so...
// TODO: implement _getFees
return FeeObject (
numberOfBlocksFast: 10 ,
numberOfBlocksAverage: 10 ,
numberOfBlocksSlow: 10 ,
fast: 1 ,
medium: 1 ,
slow: 1 ) ;
}
@ override
Future < TxData > updateSentCachedTxData ( { required TxData txData } ) async {
// TODO: [prio=low] Was not used before refactor so maybe not required(?)
return txData ;
}
@ override
Future < void > exit ( ) async {
timer ? . cancel ( ) ;
timer = null ;
await super . exit ( ) ;
Logging . instance . log ( " EpicCash_wallet exit finished " , level: LogLevel . Info ) ;
}
}
Future < String > deleteEpicWallet ( {
required String walletId ,
required SecureStorageInterface secureStore ,
} ) async {
final wallet = await secureStore . read ( key: ' ${ walletId } _wallet ' ) ;
String ? config = await secureStore . read ( key: ' ${ walletId } _config ' ) ;
if ( Platform . isIOS ) {
Directory appDir = await StackFileSystem . applicationRootDirectory ( ) ;
final path = " ${ appDir . path } /epiccash " ;
final String name = walletId . trim ( ) ;
final walletDir = ' $ path / $ name ' ;
var editConfig = jsonDecode ( config as String ) ;
editConfig [ " wallet_dir " ] = walletDir ;
config = jsonEncode ( editConfig ) ;
}
if ( wallet = = null ) {
return " Tried to delete non existent epic wallet file with walletId= $ walletId " ;
} else {
try {
return epiccash . LibEpiccash . deleteWallet (
wallet: wallet ,
config: config ! ,
) ;
} catch ( e , s ) {
Logging . instance . log ( " $ e \n $ s " , level: LogLevel . Error ) ;
return " deleteEpicWallet( $ walletId ) failed... " ;
}
}
2023-09-18 21:28:31 +00:00
}