@ -9,6 +9,9 @@
# include "camount.h"
# include "Model/ChatItem.h"
# include "DataStore/DataStore.h"
# include <future>
# include <vector>
# include <thread>
ChatModel * chatModel = new ChatModel ( ) ;
Chat * chat = new Chat ( ) ;
@ -19,12 +22,12 @@ using json = nlohmann::json;
Controller : : Controller ( MainWindow * main )
{
auto cl = new ConnectionLoader ( main , this ) ;
qDebug ( ) < < __func__ < < " : cl= " < < cl < < endl ;
//qDebug() << __func__ << ": cl=" << cl << endl;
// Execute the load connection async, so we can set up the rest of RPC properly.
QTimer : : singleShot ( 1 , [ = ] ( ) { cl - > loadConnection ( ) ; } ) ;
qDebug ( ) < < __func__ < < " after loadConnection " < < endl ;
// qDebug() << __func__ << "after loadConnection" << endl;
this - > main = main ;
this - > ui = main - > ui ;
@ -32,8 +35,8 @@ Controller::Controller(MainWindow* main)
auto current_server = Settings : : getInstance ( ) - > getSettings ( ) . server ;
main - > ui - > current_server - > setText ( current_server ) ;
auto stickyServer = Settings : : getInstance ( ) - > getSettings ( ) . stickyServer ;
main - > ui - > sticky_server - > setText ( stickyServer ? " True " : " False " ) ;
bool isStickyServerEnabled = Settings : : getInstance ( ) - > getUseStickyServer ( ) ;
main - > ui - > sticky_server - > setText ( i sS tickyServerEnabled ? " True " : " False " ) ;
// Setup balances table model
balancesTableModel = new BalancesTableModel ( main - > ui - > balancesTable ) ;
@ -122,10 +125,14 @@ void Controller::setConnection(Connection* c)
ui - > listChat - > verticalScrollBar ( ) - > setValue (
ui - > listChat - > verticalScrollBar ( ) - > maximum ( ) ) ;
//fetch amounts of notes at startup
fetchAndProcessUnspentNotes ( ) ;
}
// Build the RPC JSON Parameters for this tx
void Controller : : fillTxJsonParams ( json & allRecepients , Tx tx )
void Controller : : fillTxJsonParams ( json & allRecepients , Tx tx , bool isChatMessage )
{
Q_ASSERT ( allRecepients . is_array ( ) ) ;
@ -133,8 +140,10 @@ void Controller::fillTxJsonParams(json& allRecepients, Tx tx)
json rec = json : : object ( ) ;
//creating the JSON dust parameters in a std::vector to iterate over there during tx
std : : vector < json > dust ( 8 ) ;
dust . resize ( 8 , json : : object ( ) ) ;
std : : vector < json > dustTransactions ( 8 ) ;
for ( auto & dust : dustTransactions ) {
dust = json : : object ( ) ;
}
// Create Sietch zdust addr again to not use it twice.
// Using DataStore singelton, to store the data outside of lambda, bing bada boom :D
@ -149,11 +158,18 @@ void Controller::fillTxJsonParams(json& allRecepients, Tx tx)
// Using DataStore singelton, to store the data into the dust.
for ( uint8_t i = 0 ; i < 8 ; i + + )
{
dust . at ( i ) [ " address " ] = DataStore : : getSietchDataStore ( ) - > getData ( QString ( " Sietch " + QString ( i ) ) ) . toStdString ( ) ;
dustTransactions . at ( i ) [ " address " ] = DataStore : : getSietchDataStore ( ) - > getData ( QString ( " Sietch " + QString ( i ) ) ) . toStdString ( ) ;
}
DataStore : : getSietchDataStore ( ) - > clear ( ) ; // clears the datastore
// Only for Debugging/Testing: How many free Notes are available?
int spendableNotesCount = NoteCountDataStore : : getInstance ( ) - > getSpendableNotesCount ( ) ;
QString addressWithMaxValue = NoteCountDataStore : : getInstance ( ) - > getAddressWithMaxValue ( ) ;
// Clear NoteCountDataStore
DataStore : : getNoteCountDataStore ( ) - > clear ( ) ;
const QString possibleCharacters ( " 0123456789abcdef " ) ;
int sizerandomString = 512 ;
const int randomStringLength = sizerandomString ;
@ -168,80 +184,65 @@ void Controller::fillTxJsonParams(json& allRecepients, Tx tx)
randomString . append ( nextChar ) ;
}
dust . at ( i ) [ " memo " ] = randomString . toStdString ( ) ;
dustTransactions . at ( i ) [ " memo " ] = randomString . toStdString ( ) ;
}
CAmount balanceAvailable = getModel ( ) - > getBalVerified ( ) ;
for ( auto & it : dust )
{
it [ " amount " ] = 0 ;
}
bool isNoteAutomationEnabled = Settings : : getInstance ( ) - > getUseNoteAutomation ( ) ;
// Create more Notes if spendableNotesCount < 30 and enough funds are available and note automation is checked in settings tab
if ( spendableNotesCount < 30 & &
balanceAvailable . toDecimalString ( ) . toDouble ( ) > ( dustTransactions . size ( ) * 0.0001 ) & &
isNoteAutomationEnabled & &
isChatMessage ) {
// Create extra transaction
for ( size_t i = 0 ; i < dustTransactions . size ( ) ; + + i ) {
// Generate random memo
QString randomMemo ;
for ( int j = 0 ; j < randomStringLength ; + + j ) {
int index = QRandomGenerator : : system ( ) - > bounded ( 0 , possibleCharacters . length ( ) ) ;
randomMemo . append ( possibleCharacters . at ( index ) ) ;
}
dustTransactions . at ( i ) [ " address " ] = addressWithMaxValue . toStdString ( ) ;
dustTransactions . at ( i ) [ " amount " ] = 10000 ;
dustTransactions . at ( i ) [ " memo " ] = randomMemo . toStdString ( ) ;
}
} else {
// Set amount for real Sietch transaction to 0
for ( auto & it : dustTransactions ) {
it [ " amount " ] = 0 ;
}
}
// For each addr/amt/memo, construct the JSON and also build the confirm dialog box
for ( int i = 0 ; i < tx . toAddrs . size ( ) ; i + + )
{
auto toAddr = tx . toAddrs [ i ] ;
for ( const auto & toAddr : tx . toAddrs ) {
json rec = json : : object ( ) ;
rec [ " address " ] = toAddr . addr . toStdString ( ) ;
rec [ " amount " ] = toAddr . amount . toqint64 ( ) ;
rec [ " fee " ] = tx . fee . toqint64 ( ) ;
if ( Settings : : isZAddress ( toAddr . addr ) & & ! toAddr . memo . trimmed ( ) . isEmpty ( ) )
rec [ " memo " ] = toAddr . memo . toStdString ( ) ;
allRecepients . push_back ( rec ) ;
}
int decider = rand ( ) % 100 + 1 ; ; // random int between 1 and 100
int decider = rand ( ) % 100 + 1 ;
int dustCount = ( decider % 4 = = 3 ) ? 5 : 6 ;
if ( tx . toAddrs . size ( ) < 2 ) {
if ( decider % 4 = = 3 ) {
allRecepients . insert ( std : : begin ( allRecepients ) , {
dust . at ( 0 ) ,
dust . at ( 1 ) ,
dust . at ( 2 ) ,
dust . at ( 3 ) ,
dust . at ( 4 ) ,
dust . at ( 5 )
} ) ;
} else {
allRecepients . insert ( std : : begin ( allRecepients ) , {
dust . at ( 0 ) ,
dust . at ( 1 ) ,
dust . at ( 2 ) ,
dust . at ( 3 ) ,
dust . at ( 4 ) ,
dust . at ( 5 ) ,
dust . at ( 6 )
} ) ;
}
} else {
if ( decider % 4 = = 3 ) {
allRecepients . insert ( std : : begin ( allRecepients ) , {
dust . at ( 0 ) ,
dust . at ( 1 ) ,
dust . at ( 2 ) ,
dust . at ( 3 ) ,
dust . at ( 4 )
} ) ;
} else {
allRecepients . insert ( std : : begin ( allRecepients ) , {
dust . at ( 0 ) ,
dust . at ( 1 ) ,
dust . at ( 2 ) ,
dust . at ( 3 ) ,
dust . at ( 4 ) ,
dust . at ( 5 )
} ) ;
}
dustCount + + ;
}
for ( int i = 0 ; i < dustCount ; + + i ) {
allRecepients . insert ( allRecepients . begin ( ) , dustTransactions . at ( i ) ) ;
}
}
void Controller : : noConnection ( )
{
qDebug ( ) < < __func__ ;
//qDebug()<< __func__;
QIcon i = QApplication : : style ( ) - > standardIcon ( QStyle : : SP_MessageBoxCritical ) ;
main - > statusIcon - > setPixmap ( i . pixmap ( 16 , 16 ) ) ;
main - > statusIcon - > setToolTip ( " " ) ;
@ -274,7 +275,7 @@ void Controller::noConnection()
/// This will refresh all the balance data from hushd
void Controller : : refresh ( bool force )
{
qDebug ( ) < < __func__ ;
//qDebug()<< __func__;
if ( ! zrpc - > haveConnection ( ) )
return ;
@ -301,7 +302,7 @@ void Controller::processInfo(const json& info)
void Controller : : getInfoThenRefresh ( bool force )
{
qDebug ( ) < < __func__ ;
//qDebug()<< __func__;
if ( ! zrpc - > haveConnection ( ) )
return noConnection ( ) ;
@ -603,18 +604,28 @@ void Controller::getInfoThenRefresh(bool force)
// Prevent multiple dialog boxes, because these are called async
static bool shown = false ;
if ( ! shown & & prevCallSucceeded ) // show error only first time
{
shown = true ;
if ( ! shown & & prevCallSucceeded )
{
shown = true ;
// Check if the error is a compression flag error
if ( err . contains ( " compression " , Qt : : CaseInsensitive ) ) {
QString statusBarMessage = QObject : : tr ( " Compression error: " ) + " : \n \n " + err ;
ui - > statusBar - > showMessage ( statusBarMessage , 5000 ) ;
} else {
QString errorMessage = QObject : : tr ( " There was an error connecting to the server. Please check your internet connection. The error was " ) + " : \n \n " + err ;
QMessageBox : : critical (
main ,
main ,
QObject : : tr ( " Connection Error " ) ,
QObject : : tr ( " There was an error connecting to the server. Please check your internet connection. The error was " ) + " : \n \n " + err ,
errorMessage ,
QMessageBox : : StandardButton : : Ok
) ;
shown = false ;
}
shown = false ;
}
prevCallSucceeded = false ;
} ) ;
}
@ -635,7 +646,7 @@ void Controller::setLag(int lag)
void Controller : : refreshAddresses ( )
{
qDebug ( ) < < __func__ ;
//qDebug()<< __func__;
if ( ! zrpc - > haveConnection ( ) )
return noConnection ( ) ;
@ -680,7 +691,7 @@ void Controller::updateUI(bool anyUnconfirmed)
void Controller : : supplyUpdate ( ) {
qDebug ( ) < < __func__ < < " : updating supply " ;
// qDebug()<< __func__ << ": updating supply";
// Get the total supply and render it with thousand decimal
zrpc - > fetchSupply ( [ = ] ( const json & reply ) {
@ -701,7 +712,7 @@ void Controller::supplyUpdate() {
ui - > supply_zaddr - > setText ( " HUSH " + ( QLocale ( QLocale : : English ) . toString ( zfunds ) ) ) ;
ui - > supply_total - > setText ( " HUSH " + ( QLocale ( QLocale : : English ) . toString ( total ) ) ) ;
}
qDebug ( ) < < __func__ < < " : supply= " < < supply ;
//qDebug() << __func__ << ": supply=" << supply;
} ) ;
}
@ -890,7 +901,7 @@ void Controller::updateUIBalances()
void Controller : : refreshBalances ( )
{
qDebug ( ) < < __func__ ;
//qDebug()<< __func__;
if ( ! zrpc - > haveConnection ( ) )
return noConnection ( ) ;
@ -963,11 +974,11 @@ void printJsonValue(QTextStream& out, const nlohmann::json& j, int depth = 0) {
void Controller : : refreshTransactions ( ) {
qDebug ( ) < < __func__ ;
//qDebug()<< __func__;
if ( ! zrpc - > haveConnection ( ) )
return noConnection ( ) ;
qDebug ( ) < < __func__ < < " : fetchTransactions " ;
// qDebug() << __func__ << ": fetchTransactions";
zrpc - > fetchTransactions ( [ = ] ( json reply ) {
QList < TransactionItem > txdata ;
@ -1079,7 +1090,7 @@ void Controller::refreshTransactions() {
if ( crypto_kx_seed_keypair ( pk , sk , MESSAGEAS1 ) ! = 0 )
{
main - > logger - > write ( " Keypair outgoing error " ) ;
qDebug ( ) < < " refreshTransactions: crypto_kx_seed_keypair error " ;
// qDebug() << "refreshTransactions: crypto_kx_seed_keypair error";
continue ;
}
@ -1090,7 +1101,7 @@ void Controller::refreshTransactions() {
if ( crypto_kx_server_session_keys ( server_rx , server_tx , pk , sk , pubkeyBob ) ! = 0 )
{
main - > logger - > write ( " Suspicious client public outgoing key, bail out " ) ;
qDebug ( ) < < " refreshTransactions: Suspicious client public outgoing key, aborting! " ;
// qDebug() << " refreshTransactions: Suspicious client public outgoing key, aborting!";
continue ;
}
@ -1119,13 +1130,13 @@ void Controller::refreshTransactions() {
if ( crypto_secretstream_xchacha20poly1305_init_pull ( & state , header , server_tx ) ! = 0 ) {
/* Invalid header, no need to go any further */
qDebug ( ) < < " refreshTransactions: crypto_secretstream_xchacha20poly1305_init_pull error! Invalid header " ;
// qDebug() << " refreshTransactions: crypto_secretstream_xchacha20poly1305_init_pull error! Invalid header";
continue ;
}
if ( crypto_secretstream_xchacha20poly1305_pull ( & state , decrypted , NULL , tag , MESSAGE2 , CIPHERTEXT1_LEN , NULL , 0 ) ! = 0 ) {
/* Invalid/incomplete/corrupted ciphertext - abort */
qDebug ( ) < < " refreshTransactions: crypto_secretstream_xchacha20poly1305_pull error! Invalid ciphertext " ;
// qDebug() << " refreshTransactions: crypto_secretstream_xchacha20poly1305_pull error! Invalid ciphertext";
continue ;
}
@ -1155,7 +1166,7 @@ void Controller::refreshTransactions() {
false
) ;
qDebug ( ) < < " refreshTransactions: adding chatItem with memodecrypt= " < < memodecrypt ;
// qDebug() << " refreshTransactions: adding chatItem with memodecrypt=" << memodecrypt;
DataStore : : getChatDataStore ( ) - > setData ( ChatIDGenerator : : getInstance ( ) - > generateID ( item ) , item ) ;
// updateUIBalances();
}
@ -1269,8 +1280,8 @@ void Controller::refreshTransactions() {
int position = it [ " position " ] . get < json : : number_integer_t > ( ) ;
int ciphercheck = memo . length ( ) - crypto_secretstream_xchacha20poly1305_ABYTES ;
qDebug ( ) < < __func__ < < " : position= " < < position < < " headerbytes= " < < headerbytes
< < " ciphercheck= " < < ciphercheck < < " for memo= " < < memo ;
// qDebug() << __func__ << ": position=" << position << " headerbytes=" << headerbytes
// << " ciphercheck=" << ciphercheck << " for memo=" << memo;
if ( ( memo . startsWith ( " { " ) = = false ) & & ( headerbytes > 0 ) & & ( ciphercheck > 0 ) )
{
@ -1301,7 +1312,7 @@ void Controller::refreshTransactions() {
if ( crypto_kx_seed_keypair ( pk , sk , MESSAGEAS1 ) ! = 0 )
{
main - > logger - > write ( " Suspicious outgoing key pair, bail out " ) ;
qDebug ( ) < < " refreshTransactions: (incoming) crypto_kx_seed_keypair error! " ;
// qDebug() << " refreshTransactions: (incoming) crypto_kx_seed_keypair error!";
continue ;
}
@ -1312,7 +1323,7 @@ void Controller::refreshTransactions() {
if ( crypto_kx_client_session_keys ( client_rx , client_tx , pk , sk , pubkeyBob ) ! = 0 )
{
main - > logger - > write ( " Suspicious client public incoming key, bail out " ) ;
qDebug ( ) < < " refreshTransactions: (incoming) crypto_kx_client_session_keys error! " ;
// qDebug() << " refreshTransactions: (incoming) crypto_kx_client_session_keys error!";
continue ;
}
@ -1340,13 +1351,13 @@ void Controller::refreshTransactions() {
// crypto_secretstream_xchacha20poly1305_keygen(client_rx);
if ( crypto_secretstream_xchacha20poly1305_init_pull ( & state , header , client_rx ) ! = 0 ) {
main - > logger - > write ( " Invalid header incoming, no need to go any further " ) ;
qDebug ( ) < < " refreshTransactions: (incoming) crypto_secretstream_xchacha20poly1305_init_pull error! memo= " < < memo ;
//qDebug() <<" refreshTransactions: (incoming) crypto_secretstream_xchacha20poly1305_init_pull error! memo=" << memo;
continue ;
}
if ( crypto_secretstream_xchacha20poly1305_pull ( & state , decrypted , NULL , tag , MESSAGE2 , CIPHERTEXT1_LEN , NULL , 0 ) ! = 0 ) {
main - > logger - > write ( " Invalid/incomplete/corrupted ciphertext - abort " ) ;
qDebug ( ) < < " refreshTransactions: (incoming) crypto_secretstream_xchacha20poly1305_pull error! memo= " < < memo ;
// qDebug() << " refreshTransactions: (incoming) crypto_secretstream_xchacha20poly1305_pull error! memo=" << memo;
continue ;
}
@ -1374,11 +1385,11 @@ void Controller::refreshTransactions() {
) ;
auto iid = ChatIDGenerator : : getInstance ( ) - > generateID ( item ) ;
qDebug ( ) < < " refreshTransactions: adding chatItem with item id= " < < iid < < " memodecrypt= " < < memodecrypt ;
// qDebug() << " refreshTransactions: adding chatItem with item id=" << iid << " memodecrypt=" << memodecrypt;
DataStore : : getChatDataStore ( ) - > setData ( iid , item ) ;
} else {
qDebug ( ) < < __func__ < < " : ignoring txid= " < < txid ;
// qDebug() << __func__ << ": ignoring txid="<< txid;
}
} else {
@ -1398,7 +1409,7 @@ void Controller::refreshTransactions() {
isContact
) ;
auto iid = ChatIDGenerator : : getInstance ( ) - > generateID ( item ) ;
qDebug ( ) < < " refreshTransactions: adding chatItem for initial CR with item id= " < < iid < < " memo=' " < < memo < < " ' " ;
// qDebug() << " refreshTransactions: adding chatItem for initial CR with item id="<< iid << " memo='" << memo << "'";
DataStore : : getChatDataStore ( ) - > setData ( iid , item ) ;
}
}
@ -1425,7 +1436,7 @@ void Controller::refreshTransactions() {
// Update model data, which updates the table view
transactionsTableModel - > replaceData ( txdata ) ;
qDebug ( ) < < __func__ < < " : calling renderChatBox " ;
// qDebug() << __func__ << ": calling renderChatBox";
chat - > renderChatBox ( ui , ui - > listChat , ui - > memoSizeChat ) ;
ui - > listChat - > verticalScrollBar ( ) - > setValue ( ui - > listChat - > verticalScrollBar ( ) - > maximum ( ) ) ;
@ -1435,7 +1446,7 @@ void Controller::refreshTransactions() {
void Controller : : refreshChat ( QListView * listWidget , QLabel * label )
{
qDebug ( ) < < __func__ < < " : calling renderChatBox " ;
// qDebug() << __func__ << ": calling renderChatBox";
chat - > renderChatBox ( ui , listWidget , label ) ;
ui - > listChat - > verticalScrollBar ( ) - > setValue ( ui - > listChat - > verticalScrollBar ( ) - > maximum ( ) ) ;
@ -1507,7 +1518,7 @@ void Controller::unlockIfEncrypted(std::function<void(void)> cb, std::function<v
*/
void Controller : : executeStandardUITransaction ( Tx tx )
{
executeTransaction ( tx , [ = ] ( QString txid ) {
executeTransaction ( tx , false , [ = ] ( QString txid ) {
ui - > statusBar - > showMessage ( Settings : : txidStatusMessage + " " + txid ) ;
} ,
[ = ] ( QString opid , QString errStr ) {
@ -1528,16 +1539,18 @@ void Controller::executeStandardUITransaction(Tx tx)
) ;
}
// Execute a transaction!
void Controller : : executeTransaction ( Tx tx ,
void Controller : : executeTransaction ( Tx tx , bool isChatMessage ,
const std : : function < void ( QString txid ) > submitted ,
const std : : function < void ( QString txid , QString errStr ) > error )
const std : : function < void ( QString txid , QString errStr ) > error )
{
// Refresh the available unspent notes
fetchAndProcessUnspentNotes ( ) ;
unlockIfEncrypted ( [ = ] ( ) {
// First, create the json params
json params = json : : array ( ) ;
fillTxJsonParams ( params , tx ) ;
fillTxJsonParams ( params , tx , isChatMessage ) ;
std : : cout < < std : : setw ( 2 ) < < params < < std : : endl ;
zrpc - > sendTransaction ( QString : : fromStdString ( params . dump ( ) ) , [ = ] ( const json & reply ) {
@ -1559,19 +1572,18 @@ void Controller::executeTransaction(Tx tx,
} ) ;
}
void Controller : : checkForUpdate ( bool silent )
{
qDebug ( ) < < __func__ ;
// qDebug()<< __func__;
// No checking for updates, needs testing with Gitea
return ;
if ( ! zrpc - > haveConnection ( ) )
return noConnection ( ) ;
QUrl cmc URL( " https://git.hush.is/repos/MyH ush/SilentDragonLite/releases " ) ;
QUrl gitea URL( " https://git.hush.is/repos/h ush/SilentDragonLite/releases " ) ;
QNetworkRequest req ;
req . setUrl ( cmc URL) ;
req . setUrl ( gitea URL) ;
QNetworkAccessManager * manager = new QNetworkAccessManager ( this - > main ) ;
QNetworkReply * reply = manager - > get ( req ) ;
@ -1653,7 +1665,7 @@ void Controller::checkForUpdate(bool silent)
// Get the hush->USD price from coinmarketcap using their API
void Controller : : refreshHUSHPrice ( )
{
qDebug ( ) < < __func__ ;
// qDebug()<< __func__;
if ( ! zrpc - > haveConnection ( ) )
return ;
@ -2009,7 +2021,7 @@ void Controller::shutdownhushd()
// Save the wallet and exit the lightclient library cleanly.
if ( ! zrpc ) {
zrpc = new LiteInterface ( ) ;
qDebug ( ) < < __func__ < < " : created new rpc connection zrpc= " < < zrpc ;
// qDebug() << __func__ << ": created new rpc connection zrpc=" << zrpc;
}
if ( zrpc & & zrpc - > haveConnection ( ) )
@ -2076,3 +2088,42 @@ QString Controller::getDefaultTAddress()
return QString ( ) ;
}
void Controller : : fetchAndProcessUnspentNotes ( ) {
zrpc - > fetchUnspent ( [ = ] ( json reply ) {
if ( reply . find ( " unspent_notes " ) = = reply . end ( ) | | ! reply [ " unspent_notes " ] . is_array ( ) ) {
qDebug ( ) < < " Fehler: 'unspent_notes' fehlt oder ist kein Array " ;
return ;
}
int spendableNotesCount = 0 ;
std : : map < std : : string , int > addressValues ;
std : : string addressWithMaxValue ;
int maxValue = 0 ;
for ( const auto & note : reply [ " unspent_notes " ] ) {
if ( note . find ( " spendable " ) ! = note . end ( ) & & note . find ( " value " ) ! = note . end ( ) & &
note [ " spendable " ] . is_boolean ( ) & & note [ " value " ] . is_number_integer ( ) ) {
if ( note [ " spendable " ] & & note [ " value " ] > = 10000 ) {
spendableNotesCount + + ;
}
std : : string address = note [ " address " ] ;
int value = note [ " value " ] ;
addressValues [ address ] + = value ;
if ( addressValues [ address ] > maxValue ) {
maxValue = addressValues [ address ] ;
addressWithMaxValue = address ;
}
}
}
NoteCountDataStore : : getInstance ( ) - > setSpendableNotesCount ( spendableNotesCount ) ;
if ( ! addressWithMaxValue . empty ( ) ) {
NoteCountDataStore : : getInstance ( ) - > setAddressWithMaxValue ( QString : : fromStdString ( addressWithMaxValue ) , maxValue ) ;
}
} ) ;
}