diff --git a/.gitignore b/.gitignore index 6bcfe133..1f06c2ea 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ # testing /coverage +/testdata backup.zip # production @@ -23,4 +24,4 @@ yarn-debug.log* yarn-error.log* # generated code -/src/protobuf/** \ No newline at end of file +/src/protobuf/** diff --git a/src/HavenoDaemon.test.ts b/src/HavenoDaemon.test.ts index f5d3d362..2882fc0f 100644 --- a/src/HavenoDaemon.test.ts +++ b/src/HavenoDaemon.test.ts @@ -5,7 +5,7 @@ import {HavenoDaemon} from "./HavenoDaemon"; import {HavenoUtils} from "./utils/HavenoUtils"; import * as grpcWeb from 'grpc-web'; import {MarketPriceInfo, NotificationMessage, OfferInfo, TradeInfo, UrlConnection, XmrBalanceInfo} from './protobuf/grpc_pb'; // TODO (woodser): better names; haveno_grpc_pb, haveno_pb -import {Attachment, DisputeResult, PaymentMethod, PaymentAccount} from './protobuf/pb_pb'; +import {Attachment, DisputeResult, PaymentMethod, PaymentAccount, MoneroNodeSettings} from './protobuf/pb_pb'; import {XmrDestination, XmrTx, XmrIncomingTransfer, XmrOutgoingTransfer} from './protobuf/grpc_pb'; import AuthenticationStatus = UrlConnection.AuthenticationStatus; import OnlineStatus = UrlConnection.OnlineStatus; @@ -29,6 +29,7 @@ const console = require('console'); // import console because jest swallows mess const TestConfig = { logLevel: 0, moneroBinsDir: "../haveno/.localnet", + testDataDir: "./testdata", networkType: monerojs.MoneroNetworkType.STAGENET, haveno: { path: "../haveno", @@ -215,7 +216,7 @@ test("Can manage an account", async () => { // create account let password = "testPassword"; await charlie.createAccount(password); - await charlie.getBalances(); + if (await charlie.isConnectedToMonero()) await charlie.getBalances(); // only connected if local node running assert(await charlie.accountExists()); assert(await charlie.isAccountOpen()); @@ -267,8 +268,7 @@ test("Can manage an account", async () => { assert(await charlie.isAccountOpen()); // backup account to zip file - let rootDir = process.cwd(); - let zipFile = rootDir + "/backup.zip"; + let zipFile = TestConfig.testDataDir + "/backup.zip"; let stream = fs.createWriteStream(zipFile); let size = await charlie.backupAccount(stream); stream.end(); @@ -321,7 +321,7 @@ test("Can manage Monero daemon connections", async () => { charlie = await initHavenoDaemon(); // test default connections - let monerodUrl1 = "http://localhost:38081"; // TODO: (woodser): move to config + let monerodUrl1 = "http://127.0.0.1:38081"; // TODO: (woodser): move to config let monerodUrl2 = "http://haveno.exchange:38081"; let connections: UrlConnection[] = await charlie.getMoneroConnections(); testConnection(getConnection(connections, monerodUrl1)!, monerodUrl1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1); @@ -357,7 +357,7 @@ test("Can manage Monero daemon connections", async () => { "--" + monerojs.MoneroNetworkType.toString(TestConfig.networkType).toLowerCase(), "--no-igd", "--hide-my-port", - "--data-dir", TestConfig.moneroBinsDir + "/node1", + "--data-dir", TestConfig.moneroBinsDir + "/stagenet/testnode", "--p2p-bind-port", "58080", "--rpc-bind-port", "58081", "--rpc-login", "superuser:abctesting123", @@ -465,6 +465,96 @@ test("Can manage Monero daemon connections", async () => { if (err) throw err; }); +test("Can start and stop a local Monero node", async() => { + + // expect error stopping local node + try { + await alice.stopMoneroNode(); + HavenoUtils.log(1, "Running local Monero node stopped"); + await alice.stopMoneroNode(); // stop 2nd time to force error + throw new Error("should have thrown"); + } catch (err) { + if (err.message !== "Local Monero node is not running" && + err.message !== "Cannot stop local Monero node because we don't own its process") { + throw new Error("Unexpected error: " + err.message); + } + } + + let isMoneroNodeRunning = await alice.isMoneroNodeRunning(); + if (isMoneroNodeRunning) { + HavenoUtils.log(0, "Warning: local Monero node is already running, skipping start and stop local Monero node tests"); + + // expect error due to existing running node + let newSettings = new MoneroNodeSettings(); + try { + await alice.startMoneroNode(newSettings); + throw new Error("should have thrown"); + } catch (err) { + if (err.message !== "Local Monero node already running") throw new Error("Unexpected error: " + err.message); + } + + } else { + + // expect error when passing in bad arguments + let badSettings = new MoneroNodeSettings(); + badSettings.setStartupFlagsList(["--invalid-flag"]); + try { + await alice.startMoneroNode(badSettings); + throw new Error("should have thrown"); + } catch (err) { + if (!err.message.startsWith("Failed to start monerod:")) throw new Error("Unexpected error: "); + } + + // expect successful start with custom settings + let connectionsBefore = await alice.getMoneroConnections(); + let settings: MoneroNodeSettings = new MoneroNodeSettings(); + let dataDir = TestConfig.moneroBinsDir + "/stagenet/node1"; + let logFile = dataDir + "/test.log"; + let p2pPort = 38080; + let rpcPort = 38081; + settings.setBlockchainPath(dataDir); + settings.setStartupFlagsList(["--log-file", logFile, "--p2p-bind-port", p2pPort.toString(), "--rpc-bind-port", rpcPort.toString(), "--no-zmq"]); + await alice.startMoneroNode(settings); + isMoneroNodeRunning = await alice.isMoneroNodeRunning(); + assert(isMoneroNodeRunning); + + // expect settings are updated + let settingsAfter = await alice.getMoneroNodeSettings(); + testMoneroNodeSettings(settings, settingsAfter!); + + // expect connections to be unmodified by local node + let connectionsAfter = await alice.getMoneroConnections(); + assert(connectionsBefore.length === connectionsAfter.length); + + // expect connection to local monero node to succeed + let rpcUrl = "http://127.0.0.1:" + rpcPort.toString(); + let daemon = await monerojs.connectToDaemonRpc(rpcUrl, "superuser", "abctesting123"); + let height = await daemon.getHeight(); + assert(height >= 0); + + // expect error due to existing running node + let newSettings = new MoneroNodeSettings(); + try { + await alice.startMoneroNode(newSettings); + throw new Error("should have thrown"); + } catch (err) { + if (err.message !== "Local Monero node already running") throw new Error("Unexpected error: " + err.message); + } + + // expect stopped node + await alice.stopMoneroNode(); + isMoneroNodeRunning = await alice.isMoneroNodeRunning(); + assert(!isMoneroNodeRunning); + try { + daemon = await monerojs.connectToDaemonRpc(rpcUrl); + height = await daemon.getHeight(); + throw new Error("should have thrown"); + } catch (err) { + if (err.message !== "RequestError: Error: connect ECONNREFUSED 127.0.0.1:" + rpcPort.toString()) throw new Error("Unexpected error: " + err.message); + } + } +}); + // test wallet balances, transactions, deposit addresses, create and relay txs test("Has a Monero wallet", async () => { @@ -1431,6 +1521,7 @@ async function initHavenoDaemon(config?: any): Promise { }); }); } + } /** @@ -1800,9 +1891,9 @@ async function testTradeChat(tradeId: string, alice: HavenoDaemon, bob: HavenoDa // trade chat should be in initial state let messages = await alice.getChatMessages(tradeId); - assert(messages.length == 0); + assert(messages.length === 0); messages = await bob.getChatMessages(tradeId); - assert(messages.length == 0); + assert(messages.length === 0); // add notification handlers and send some messages let aliceNotifications: NotificationMessage[] = []; @@ -1850,7 +1941,7 @@ async function testTradeChat(tradeId: string, alice: HavenoDaemon, bob: HavenoDa expect(messages[1].getMessage()).toEqual(aliceMsg); expect(messages[2].getMessage()).toEqual(bobMsg); for (var i = 0; i < msgs.length; i++) { - expect(messages[i+offset].getMessage()).toEqual(msgs[i]); + expect(messages[i + offset].getMessage()).toEqual(msgs[i]); } chatNotifications = getNotifications(bobNotifications, NotificationMessage.NotificationType.CHAT_MESSAGE); @@ -1860,4 +1951,10 @@ async function testTradeChat(tradeId: string, alice: HavenoDaemon, bob: HavenoDa for (var i = 0; i < msgs.length; i++) { expect(chatNotifications[i + offset].getChatMessage()?.getMessage()).toEqual(msgs[i]); } +} + +function testMoneroNodeSettings(settingsBefore: MoneroNodeSettings, settingsAfter: MoneroNodeSettings) { + expect(settingsBefore.getBlockchainPath()).toEqual(settingsAfter.getBlockchainPath()); + expect(settingsBefore.getBootstrapUrl()).toEqual(settingsAfter.getBootstrapUrl()); + expect(settingsBefore.getStartupFlagsList()).toEqual(settingsAfter.getStartupFlagsList()); } \ No newline at end of file diff --git a/src/HavenoDaemon.ts b/src/HavenoDaemon.ts index d7de8230..bcd52635 100644 --- a/src/HavenoDaemon.ts +++ b/src/HavenoDaemon.ts @@ -1,9 +1,9 @@ import {HavenoUtils} from "./utils/HavenoUtils"; import {TaskLooper} from "./utils/TaskLooper"; import * as grpcWeb from 'grpc-web'; -import {GetVersionClient, AccountClient, MoneroConnectionsClient, DisputesClient, DisputeAgentsClient, NotificationsClient, WalletsClient, PriceClient, OffersClient, PaymentAccountsClient, TradesClient, ShutdownServerClient} from './protobuf/GrpcServiceClientPb'; -import {GetVersionRequest, GetVersionReply, IsAppInitializedRequest, IsAppInitializedReply, RegisterDisputeAgentRequest, MarketPriceRequest, MarketPriceReply, MarketPricesRequest, MarketPricesReply, MarketPriceInfo, MarketDepthRequest, MarketDepthReply, MarketDepthInfo, GetBalancesRequest, GetBalancesReply, XmrBalanceInfo, GetMyOfferRequest, GetMyOfferReply, GetOffersRequest, GetOffersReply, OfferInfo, GetPaymentMethodsRequest, GetPaymentMethodsReply, GetPaymentAccountFormRequest, CreatePaymentAccountRequest, CreatePaymentAccountReply, GetPaymentAccountFormReply, GetPaymentAccountsRequest, GetPaymentAccountsReply, CreateCryptoCurrencyPaymentAccountRequest, CreateCryptoCurrencyPaymentAccountReply, CreateOfferRequest, CreateOfferReply, CancelOfferRequest, TakeOfferRequest, TakeOfferReply, TradeInfo, GetTradeRequest, GetTradeReply, GetTradesRequest, GetTradesReply, GetNewDepositSubaddressRequest, GetNewDepositSubaddressReply, ConfirmPaymentStartedRequest, ConfirmPaymentReceivedRequest, XmrTx, GetXmrTxsRequest, GetXmrTxsReply, XmrDestination, CreateXmrTxRequest, CreateXmrTxReply, RelayXmrTxRequest, RelayXmrTxReply, CreateAccountRequest, AccountExistsRequest, AccountExistsReply, DeleteAccountRequest, OpenAccountRequest, IsAccountOpenRequest, IsAccountOpenReply, CloseAccountRequest, ChangePasswordRequest, BackupAccountRequest, BackupAccountReply, RestoreAccountRequest, StopRequest, NotificationMessage, RegisterNotificationListenerRequest, SendNotificationRequest, UrlConnection, AddConnectionRequest, RemoveConnectionRequest, GetConnectionRequest, GetConnectionsRequest, SetConnectionRequest, CheckConnectionRequest, CheckConnectionsReply, CheckConnectionsRequest, StartCheckingConnectionsRequest, StopCheckingConnectionsRequest, GetBestAvailableConnectionRequest, SetAutoSwitchRequest, CheckConnectionReply, GetConnectionsReply, GetConnectionReply, GetBestAvailableConnectionReply, GetDisputeRequest, GetDisputeReply, GetDisputesRequest, GetDisputesReply, OpenDisputeRequest, ResolveDisputeRequest, SendDisputeChatMessageRequest, SendChatMessageRequest, GetChatMessagesRequest, GetChatMessagesReply} from './protobuf/grpc_pb'; -import {PaymentMethod, PaymentAccount, AvailabilityResult, Attachment, DisputeResult, Dispute, ChatMessage} from './protobuf/pb_pb'; +import {GetVersionClient, AccountClient, MoneroConnectionsClient, DisputesClient, DisputeAgentsClient, NotificationsClient, WalletsClient, PriceClient, OffersClient, PaymentAccountsClient, TradesClient, ShutdownServerClient, MoneroNodeClient} from './protobuf/GrpcServiceClientPb'; +import {GetVersionRequest, GetVersionReply, IsAppInitializedRequest, IsAppInitializedReply, RegisterDisputeAgentRequest, MarketPriceRequest, MarketPriceReply, MarketPricesRequest, MarketPricesReply, MarketPriceInfo, MarketDepthRequest, MarketDepthReply, MarketDepthInfo, GetBalancesRequest, GetBalancesReply, XmrBalanceInfo, GetMyOfferRequest, GetMyOfferReply, GetOffersRequest, GetOffersReply, OfferInfo, GetPaymentMethodsRequest, GetPaymentMethodsReply, GetPaymentAccountFormRequest, CreatePaymentAccountRequest, CreatePaymentAccountReply, GetPaymentAccountFormReply, GetPaymentAccountsRequest, GetPaymentAccountsReply, CreateCryptoCurrencyPaymentAccountRequest, CreateCryptoCurrencyPaymentAccountReply, CreateOfferRequest, CreateOfferReply, CancelOfferRequest, TakeOfferRequest, TakeOfferReply, TradeInfo, GetTradeRequest, GetTradeReply, GetTradesRequest, GetTradesReply, GetNewDepositSubaddressRequest, GetNewDepositSubaddressReply, ConfirmPaymentStartedRequest, ConfirmPaymentReceivedRequest, XmrTx, GetXmrTxsRequest, GetXmrTxsReply, XmrDestination, CreateXmrTxRequest, CreateXmrTxReply, RelayXmrTxRequest, RelayXmrTxReply, CreateAccountRequest, AccountExistsRequest, AccountExistsReply, DeleteAccountRequest, OpenAccountRequest, IsAccountOpenRequest, IsAccountOpenReply, CloseAccountRequest, ChangePasswordRequest, BackupAccountRequest, BackupAccountReply, RestoreAccountRequest, StopRequest, NotificationMessage, RegisterNotificationListenerRequest, SendNotificationRequest, UrlConnection, AddConnectionRequest, RemoveConnectionRequest, GetConnectionRequest, GetConnectionsRequest, SetConnectionRequest, CheckConnectionRequest, CheckConnectionsReply, CheckConnectionsRequest, StartCheckingConnectionsRequest, StopCheckingConnectionsRequest, GetBestAvailableConnectionRequest, SetAutoSwitchRequest, CheckConnectionReply, GetConnectionsReply, GetConnectionReply, GetBestAvailableConnectionReply, GetDisputeRequest, GetDisputeReply, GetDisputesRequest, GetDisputesReply, OpenDisputeRequest, ResolveDisputeRequest, SendDisputeChatMessageRequest, SendChatMessageRequest, GetChatMessagesRequest, GetChatMessagesReply, StartMoneroNodeRequest, StopMoneroNodeRequest, IsMoneroNodeRunningRequest, IsMoneroNodeRunningReply, GetMoneroNodeSettingsRequest, GetMoneroNodeSettingsReply} from './protobuf/grpc_pb'; +import {PaymentMethod, PaymentAccount, AvailabilityResult, Attachment, DisputeResult, Dispute, ChatMessage, MoneroNodeSettings} from './protobuf/pb_pb'; const console = require('console'); @@ -19,6 +19,7 @@ class HavenoDaemon { _disputesClient: DisputesClient; _notificationsClient: NotificationsClient; _moneroConnectionsClient: MoneroConnectionsClient; + _moneroNodeClient: MoneroNodeClient; _walletsClient: WalletsClient; _priceClient: PriceClient; _paymentAccountsClient: PaymentAccountsClient; @@ -56,7 +57,8 @@ class HavenoDaemon { this._password = password; this._getVersionClient = new GetVersionClient(this._url); this._accountClient = new AccountClient(this._url); - this._moneroConnectionsClient = new MoneroConnectionsClient(this._url) + this._moneroConnectionsClient = new MoneroConnectionsClient(this._url); + this._moneroNodeClient = new MoneroNodeClient(this._url); this._disputeAgentsClient = new DisputeAgentsClient(this._url); this._disputesClient = new DisputesClient(this._url); this._walletsClient = new WalletsClient(this._url); @@ -593,6 +595,62 @@ class HavenoDaemon { }); }); } + + /** + * Returns whether daemon is running a local monero node. + */ + async isMoneroNodeRunning(): Promise { + let that = this; + return new Promise(function(resolve, reject) { + that._moneroNodeClient.isMoneroNodeRunning(new IsMoneroNodeRunningRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: IsMoneroNodeRunningReply) { + if (err) reject(err); + else resolve(response.getIsRunning()); + }); + }); + } + + /** + * Gets the current local monero node settings. + */ + async getMoneroNodeSettings(): Promise { + let that = this; + return new Promise(function(resolve, reject) { + let request = new GetMoneroNodeSettingsRequest(); + that._moneroNodeClient.getMoneroNodeSettings(request, {password: that._password}, function(err: grpcWeb.RpcError, response: GetMoneroNodeSettingsReply) { + if (err) reject(err); + else resolve(response.getSettings()); + }); + }); + } + + /** + * Starts the local monero node. + * + * @param {MoneroNodeSettings} settings - the settings to start the local node with + */ + async startMoneroNode(settings: MoneroNodeSettings): Promise { + let that = this; + return new Promise(function(resolve, reject) { + let request = new StartMoneroNodeRequest().setSettings(settings); + that._moneroNodeClient.startMoneroNode(request, {password: that._password}, function(err: grpcWeb.RpcError) { + if (err) reject(err); + else resolve(); + }); + }); + } + + /** + * Stops the local monero node. + */ + async stopMoneroNode(): Promise { + let that = this; + return new Promise(function(resolve, reject) { + that._moneroNodeClient.stopMoneroNode(new StopMoneroNodeRequest(), {password: that._password}, function(err: grpcWeb.RpcError) { + if (err) reject(err); + else resolve(); + }); + }); + } /** * Register as a dispute agent.