mirror of
https://github.com/haveno-dex/haveno-ts.git
synced 2025-01-12 15:59:56 -05:00
Add API functions to initialize Haveno account (#64)
Co-authored-by: woodser@protonmail.com
This commit is contained in:
parent
f53b9c3437
commit
0df355faa7
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,6 +7,7 @@
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
backup.zip
|
||||
|
||||
# production
|
||||
/build
|
||||
@ -17,7 +18,6 @@
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
@ -4,11 +4,11 @@
|
||||
import {HavenoDaemon} from "./HavenoDaemon";
|
||||
import {HavenoUtils} from "./utils/HavenoUtils";
|
||||
import * as grpcWeb from 'grpc-web';
|
||||
import {MarketPriceInfo, NotificationMessage, OfferInfo, TradeInfo, UriConnection, XmrBalanceInfo} from './protobuf/grpc_pb'; // TODO (woodser): better names; haveno_grpc_pb, haveno_pb
|
||||
import {MarketPriceInfo, NotificationMessage, OfferInfo, TradeInfo, UrlConnection, XmrBalanceInfo} from './protobuf/grpc_pb'; // TODO (woodser): better names; haveno_grpc_pb, haveno_pb
|
||||
import {PaymentAccount} from './protobuf/pb_pb';
|
||||
import {XmrDestination, XmrTx, XmrIncomingTransfer, XmrOutgoingTransfer} from './protobuf/grpc_pb';
|
||||
import AuthenticationStatus = UriConnection.AuthenticationStatus;
|
||||
import OnlineStatus = UriConnection.OnlineStatus;
|
||||
import AuthenticationStatus = UrlConnection.AuthenticationStatus;
|
||||
import OnlineStatus = UrlConnection.OnlineStatus;
|
||||
|
||||
// import monero-javascript
|
||||
const monerojs = require("monero-javascript"); // TODO (woodser): support typescript and `npm install @types/monero-javascript` in monero-javascript
|
||||
@ -19,18 +19,15 @@ const MoneroUtils = monerojs.MoneroUtils;
|
||||
const TaskLooper = monerojs.TaskLooper;
|
||||
|
||||
// other required imports
|
||||
const console = require('console'); // import console because jest swallows messages in real time
|
||||
const assert = require("assert");
|
||||
const fs = require('fs');
|
||||
const net = require('net');
|
||||
const assert = require("assert");
|
||||
const console = require('console'); // import console because jest swallows messages in real time
|
||||
|
||||
// ------------------------------ TEST CONFIG ---------------------------------
|
||||
|
||||
// test config
|
||||
const TestConfig = {
|
||||
logging: {
|
||||
level: 0, // set log level (gets more verbose increasing from 0)
|
||||
logProcessOutput: false // enable or disable logging process output
|
||||
},
|
||||
logLevel: 0,
|
||||
moneroBinsDir: "../haveno/.localnet",
|
||||
networkType: monerojs.MoneroNetworkType.STAGENET,
|
||||
haveno: {
|
||||
@ -38,45 +35,52 @@ const TestConfig = {
|
||||
version: "1.6.2"
|
||||
},
|
||||
monerod: {
|
||||
uri: "http://localhost:38081",
|
||||
url: "http://localhost:38081",
|
||||
username: "superuser",
|
||||
password: "abctesting123"
|
||||
},
|
||||
monerod2: {
|
||||
uri: "http://localhost:58081",
|
||||
url: "http://localhost:58081",
|
||||
username: "superuser",
|
||||
password: "abctesting123"
|
||||
},
|
||||
fundingWallet: {
|
||||
uri: "http://localhost:38084",
|
||||
url: "http://localhost:38084",
|
||||
username: "rpc_user",
|
||||
password: "abc123",
|
||||
defaultPath: "test_funding_wallet",
|
||||
minimumFunding: BigInt("5000000000000")
|
||||
},
|
||||
arbitrator: {
|
||||
logProcessOutput: false,
|
||||
appName: "haveno-XMR_STAGENET_arbitrator",
|
||||
uri: "http://localhost:8079",
|
||||
password: "apitest",
|
||||
walletUsername: "rpc_user",
|
||||
walletPassword: "abc123"
|
||||
defaultHavenod: {
|
||||
logProcessOutput: false, // log output for processes started by tests (except arbitrator, alice, and bob which are configured separately)
|
||||
apiPassword: "apitest",
|
||||
walletUsername: "haveno_user",
|
||||
walletDefaultPassword: "password", // only used if account password not set
|
||||
accountPasswordRequired: true,
|
||||
accountPassword: "abctesting789",
|
||||
autoLogin: true
|
||||
},
|
||||
alice: {
|
||||
logProcessOutput: false,
|
||||
appName: "haveno-XMR_STAGENET_alice",
|
||||
uri: "http://localhost:8080",
|
||||
password: "apitest",
|
||||
walletUri: "http://127.0.0.1:38091",
|
||||
walletUsername: "rpc_user",
|
||||
walletPassword: "abc123"
|
||||
},
|
||||
bob: {
|
||||
logProcessOutput: false,
|
||||
appName: "haveno-XMR_STAGENET_bob",
|
||||
uri: "http://localhost:8081",
|
||||
password: "apitest",
|
||||
},
|
||||
startupHavenods: [{
|
||||
appName: "haveno-XMR_STAGENET_arbitrator", // arbritrator
|
||||
logProcessOutput: false,
|
||||
url: "http://localhost:8079",
|
||||
accountPasswordRequired: false,
|
||||
accountPassword: "abctesting123",
|
||||
}, {
|
||||
appName: "haveno-XMR_STAGENET_alice", // alice
|
||||
logProcessOutput: false,
|
||||
url: "http://localhost:8080",
|
||||
accountPasswordRequired: false,
|
||||
accountPassword: "abctesting456",
|
||||
walletUrl: "http://127.0.0.1:38091",
|
||||
}, {
|
||||
appName: "haveno-XMR_STAGENET_bob", // bob
|
||||
logProcessOutput: false,
|
||||
url: "http://localhost:8081",
|
||||
accountPasswordRequired: false,
|
||||
accountPassword: "abctesting789",
|
||||
}
|
||||
],
|
||||
maxFee: BigInt("75000000000"),
|
||||
walletSyncPeriodMs: 5000, // TODO (woodser): auto adjust higher if using remote connection
|
||||
daemonPollPeriodMs: 15000,
|
||||
@ -99,7 +103,8 @@ const TestConfig = {
|
||||
["8085", ["10004", "7780"]],
|
||||
["8086", ["10005", "7781"]],
|
||||
]),
|
||||
devPrivilegePrivKey: "6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a" // from DEV_PRIVILEGE_PRIV_KEY
|
||||
devPrivilegePrivKey: "6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a", // from DEV_PRIVILEGE_PRIV_KEY
|
||||
timeout: 900000 // timeout in ms for all tests to complete (15 minutes)
|
||||
};
|
||||
|
||||
interface TxContext {
|
||||
@ -107,6 +112,7 @@ interface TxContext {
|
||||
}
|
||||
|
||||
// clients
|
||||
let startupHavenods: HavenoDaemon[] = [];
|
||||
let arbitrator: HavenoDaemon;
|
||||
let alice: HavenoDaemon;
|
||||
let bob: HavenoDaemon;
|
||||
@ -118,42 +124,41 @@ let aliceWallet: any;
|
||||
const HAVENO_PROCESSES: HavenoDaemon[] = [];
|
||||
const HAVENO_PROCESS_PORTS: string[] = [];
|
||||
|
||||
// other config
|
||||
const OFFLINE_ERR_MSG = "Http response at 400 or 500 level";
|
||||
|
||||
// -------------------------- BEFORE / AFTER TESTS ----------------------------
|
||||
|
||||
jest.setTimeout(TestConfig.timeout);
|
||||
|
||||
beforeAll(async () => {
|
||||
|
||||
// set log level for tests
|
||||
HavenoUtils.setLogLevel(TestConfig.logging.level);
|
||||
HavenoUtils.setLogLevel(TestConfig.logLevel);
|
||||
|
||||
// connect to arbitrator, alice, and bob or start as child processes
|
||||
let daemonPromises = [];
|
||||
daemonPromises.push(initHavenoDaemon(TestConfig.arbitrator));
|
||||
daemonPromises.push(initHavenoDaemon(TestConfig.alice));
|
||||
daemonPromises.push(initHavenoDaemon(TestConfig.bob));
|
||||
let daemons = await Promise.allSettled(daemonPromises);
|
||||
if (daemons[0].status === "fulfilled") arbitrator = (daemons[0] as PromiseFulfilledResult<HavenoDaemon>).value;
|
||||
else throw new Error((daemons[0] as PromiseRejectedResult).reason);
|
||||
if (daemons[1].status === "fulfilled") alice = (daemons[1] as PromiseFulfilledResult<HavenoDaemon>).value;
|
||||
else throw new Error((daemons[1] as PromiseRejectedResult).reason);
|
||||
if (daemons[2].status === "fulfilled") bob = (daemons[2] as PromiseFulfilledResult<HavenoDaemon>).value;
|
||||
else throw new Error((daemons[2] as PromiseRejectedResult).reason);
|
||||
// start configured haveno daemons
|
||||
let promises = [];
|
||||
for (let config of TestConfig.startupHavenods) promises.push(initHavenoDaemon(config));
|
||||
for (let settledPromise of await Promise.allSettled(promises)) {
|
||||
if (settledPromise.status !== "fulfilled") throw new Error((settledPromise as PromiseRejectedResult).reason);
|
||||
startupHavenods.push((settledPromise as PromiseFulfilledResult<HavenoDaemon>).value);
|
||||
}
|
||||
|
||||
// assign arbitrator alice, bob
|
||||
arbitrator = startupHavenods[0];
|
||||
alice = startupHavenods[1];
|
||||
bob = startupHavenods[2];
|
||||
|
||||
// register arbitrator as dispute agents
|
||||
await arbitrator.registerDisputeAgent("mediator", TestConfig.devPrivilegePrivKey);
|
||||
await arbitrator.registerDisputeAgent("refundagent", TestConfig.devPrivilegePrivKey);
|
||||
|
||||
// connect monero clients
|
||||
monerod = await monerojs.connectToDaemonRpc(TestConfig.monerod.uri, TestConfig.monerod.username, TestConfig.monerod.password);
|
||||
aliceWallet = await monerojs.connectToWalletRpc(TestConfig.alice.walletUri, TestConfig.alice.walletUsername, TestConfig.alice.walletPassword);
|
||||
monerod = await monerojs.connectToDaemonRpc(TestConfig.monerod.url, TestConfig.monerod.username, TestConfig.monerod.password);
|
||||
aliceWallet = await monerojs.connectToWalletRpc(TestConfig.startupHavenods[1].walletUrl, TestConfig.defaultHavenod.walletUsername, TestConfig.startupHavenods[1].accountPasswordRequired ? TestConfig.startupHavenods[1].accountPassword : TestConfig.defaultHavenod.walletDefaultPassword);
|
||||
|
||||
// initialize funding wallet
|
||||
await initFundingWallet();
|
||||
|
||||
// debug tools
|
||||
//for (let offer of await alice.getMyOffers("BUY")) await alice.removeOffer(offer.getId());
|
||||
//for (let offer of await alice.getMyOffers("SELL")) await alice.removeOffer(offer.getId());
|
||||
//console.log((await alice.getBalances()).getUnlockedBalance() + ", " + (await alice.getBalances()).getLockedBalance());
|
||||
//console.log((await bob.getBalances()).getUnlockedBalance() + ", " + (await bob.getBalances()).getLockedBalance());
|
||||
});
|
||||
|
||||
beforeEach(async() => {
|
||||
@ -161,15 +166,11 @@ beforeEach(async() => {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
let stopPromises = [];
|
||||
if (arbitrator && arbitrator.getProcess()) stopPromises.push(stopHavenoProcess(arbitrator));
|
||||
if (alice && alice.getProcess()) stopPromises.push(stopHavenoProcess(alice));
|
||||
if (bob && bob.getProcess()) stopPromises.push(stopHavenoProcess(bob));
|
||||
return Promise.all(stopPromises);
|
||||
let promises = [];
|
||||
for (let havenod of startupHavenods) if (havenod.getProcess()) promises.push(releaseHavenoProcess(havenod));
|
||||
return Promise.all(promises);
|
||||
});
|
||||
|
||||
jest.setTimeout(500000);
|
||||
|
||||
// ----------------------------------- TESTS ----------------------------------
|
||||
|
||||
test("Can get the version", async () => {
|
||||
@ -177,50 +178,114 @@ test("Can get the version", async () => {
|
||||
expect(version).toEqual(TestConfig.haveno.version);
|
||||
});
|
||||
|
||||
test("Can register as dispute agents", async () => {
|
||||
await arbitrator.registerDisputeAgent("mediator", TestConfig.devPrivilegePrivKey); // TODO: bisq mediator = haveno arbitrator
|
||||
await arbitrator.registerDisputeAgent("refundagent", TestConfig.devPrivilegePrivKey); // TODO: no refund agent in haveno
|
||||
|
||||
// test bad dispute agent type
|
||||
test("Can manage an account", async () => {
|
||||
let charlie: HavenoDaemon | undefined;
|
||||
let err: any;
|
||||
try {
|
||||
await arbitrator.registerDisputeAgent("unsupported type", TestConfig.devPrivilegePrivKey);
|
||||
throw new Error("should have thrown error registering bad type");
|
||||
} catch (err) {
|
||||
if (err.message !== "unknown dispute agent type 'unsupported type'") throw new Error("Unexpected error: " + err.message);
|
||||
|
||||
// start charlie without opening account
|
||||
charlie = await initHavenoDaemon({autoLogin: false});
|
||||
assert(!await charlie.accountExists());
|
||||
|
||||
// test errors when account not open
|
||||
await testAccountNotOpen(charlie);
|
||||
|
||||
// create account
|
||||
let password = "testPassword";
|
||||
await charlie.createAccount(password);
|
||||
await charlie.getBalances();
|
||||
assert(await charlie.accountExists());
|
||||
assert(await charlie.isAccountOpen());
|
||||
|
||||
// close account
|
||||
await charlie.closeAccount();
|
||||
assert(await charlie.accountExists());
|
||||
assert(!await charlie.isAccountOpen());
|
||||
await testAccountNotOpen(charlie);
|
||||
|
||||
// open account with wrong password
|
||||
try {
|
||||
await charlie.openAccount("wrongPassword");
|
||||
throw new Error("Should have failed opening account with wrong password");
|
||||
} catch (err) {
|
||||
assert.equal(err.message, "Incorrect password");
|
||||
}
|
||||
|
||||
// open account
|
||||
await charlie.openAccount(password);
|
||||
assert(await charlie.accountExists());
|
||||
assert(await charlie.isAccountOpen());
|
||||
|
||||
// restart charlie
|
||||
let charlieConfig = {appName: charlie.getAppName(), autoLogin: false}
|
||||
await releaseHavenoProcess(charlie);
|
||||
charlie = await initHavenoDaemon(charlieConfig);
|
||||
assert(await charlie.accountExists());
|
||||
assert(!await charlie.isAccountOpen());
|
||||
|
||||
// open account
|
||||
await charlie.openAccount(password);
|
||||
assert(await charlie.accountExists());
|
||||
assert(await charlie.isAccountOpen());
|
||||
|
||||
// change password
|
||||
password = "newPassword";
|
||||
await charlie.changePassword(password);
|
||||
assert(await charlie.accountExists());
|
||||
assert(await charlie.isAccountOpen());
|
||||
|
||||
// restart charlie
|
||||
await releaseHavenoProcess(charlie);
|
||||
charlie = await initHavenoDaemon(charlieConfig);
|
||||
await testAccountNotOpen(charlie);
|
||||
|
||||
// open account
|
||||
await charlie.openAccount(password);
|
||||
assert(await charlie.accountExists());
|
||||
assert(await charlie.isAccountOpen());
|
||||
|
||||
// backup account to zip file
|
||||
let rootDir = process.cwd();
|
||||
let zipFile = rootDir + "/backup.zip";
|
||||
let stream = fs.createWriteStream(zipFile);
|
||||
let size = await charlie.backupAccount(stream);
|
||||
stream.end();
|
||||
assert(size > 0);
|
||||
|
||||
// delete account which shuts down server
|
||||
await charlie.deleteAccount();
|
||||
assert(!await charlie.isConnectedToDaemon());
|
||||
await releaseHavenoProcess(charlie);
|
||||
|
||||
// restore account which shuts down server
|
||||
charlie = await initHavenoDaemon(charlieConfig);
|
||||
let zipBytes: Uint8Array = new Uint8Array(fs.readFileSync(zipFile));
|
||||
await charlie.restoreAccount(zipBytes);
|
||||
assert(!await charlie.isConnectedToDaemon());
|
||||
await releaseHavenoProcess(charlie);
|
||||
|
||||
// open restored account
|
||||
charlie = await initHavenoDaemon(charlieConfig);
|
||||
assert(await charlie.accountExists());
|
||||
await charlie.openAccount(password);
|
||||
assert(await charlie.isAccountOpen());
|
||||
} catch (err2) {
|
||||
console.log(err2);
|
||||
err = err2;
|
||||
}
|
||||
|
||||
// stop processes
|
||||
if (charlie) await releaseHavenoProcess(charlie);
|
||||
// TODO: how to delete trader app folder at end of test?
|
||||
if (err) throw err;
|
||||
|
||||
// test bad key
|
||||
try {
|
||||
await arbitrator.registerDisputeAgent("mediator", "bad key");
|
||||
throw new Error("should have thrown error registering bad key");
|
||||
} catch (err) {
|
||||
if (err.message !== "invalid registration key") throw new Error("Unexpected error: " + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
test("Can receive push notifications", async () => {
|
||||
|
||||
// add notification listener
|
||||
let notifications: NotificationMessage[] = [];
|
||||
await alice.addNotificationListener(notification => {
|
||||
notifications.push(notification);
|
||||
});
|
||||
|
||||
// send test notification
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await alice._sendNotification(new NotificationMessage()
|
||||
.setTimestamp(Date.now())
|
||||
.setTitle("Test title")
|
||||
.setMessage("Test message"));
|
||||
}
|
||||
|
||||
// test notification
|
||||
await wait(1000);
|
||||
assert.equal(3, notifications.length);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
assert(notifications[i].getTimestamp() > 0);
|
||||
assert.equal("Test title", notifications[i].getTitle());
|
||||
assert.equal("Test message", notifications[i].getMessage());
|
||||
async function testAccountNotOpen(havenod: HavenoDaemon): Promise<void> { // TODO: generalize this?
|
||||
try { await havenod.getMoneroConnections(); throw new Error("Should have thrown"); }
|
||||
catch (err) { assert.equal(err.message, "Account not open"); }
|
||||
try { await havenod.getXmrTxs(); throw new Error("Should have thrown"); }
|
||||
catch (err) { assert.equal(err.message, "Account not open"); }
|
||||
try { await havenod.getBalances(); throw new Error("Should have thrown"); }
|
||||
catch (err) { assert.equal(err.message, "Account not open"); }
|
||||
}
|
||||
});
|
||||
|
||||
@ -231,38 +296,38 @@ test("Can manage Monero daemon connections", async () => {
|
||||
try {
|
||||
|
||||
// start charlie
|
||||
charlie = await startHavenoProcess(undefined, TestConfig.logging.logProcessOutput);
|
||||
charlie = await initHavenoDaemon();
|
||||
|
||||
// test default connections
|
||||
let monerodUri1 = "http://localhost:38081"; // TODO: (woodser): move to config
|
||||
let monerodUri2 = "http://haveno.exchange:38081";
|
||||
let connections: UriConnection[] = await charlie.getMoneroConnections();
|
||||
testConnection(getConnection(connections, monerodUri1)!, monerodUri1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
|
||||
testConnection(getConnection(connections, monerodUri2)!, monerodUri2, OnlineStatus.UNKNOWN, AuthenticationStatus.NO_AUTHENTICATION, 2);
|
||||
let monerodUrl1 = "http://localhost: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);
|
||||
testConnection(getConnection(connections, monerodUrl2)!, monerodUrl2, OnlineStatus.UNKNOWN, AuthenticationStatus.NO_AUTHENTICATION, 2);
|
||||
|
||||
// test default connection
|
||||
let connection: UriConnection | undefined = await charlie.getMoneroConnection();
|
||||
testConnection(connection!, monerodUri1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
|
||||
//assert(await charlie.isMoneroConnected()); // TODO (woodser): support havenod.isConnected()?
|
||||
let connection: UrlConnection | undefined = await charlie.getMoneroConnection();
|
||||
assert(await charlie.isConnectedToMonero());
|
||||
testConnection(connection!, monerodUrl1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
|
||||
|
||||
// add a new connection
|
||||
let fooBarUri = "http://foo.bar";
|
||||
await charlie.addMoneroConnection(fooBarUri);
|
||||
let fooBarUrl = "http://foo.bar";
|
||||
await charlie.addMoneroConnection(fooBarUrl);
|
||||
connections = await charlie.getMoneroConnections();
|
||||
connection = getConnection(connections, fooBarUri);
|
||||
testConnection(connection!, fooBarUri, OnlineStatus.UNKNOWN, AuthenticationStatus.NO_AUTHENTICATION, 0);
|
||||
//connection = await charlie.getMoneroConnection(uri); TODO (woodser): allow getting connection by uri?
|
||||
connection = getConnection(connections, fooBarUrl);
|
||||
testConnection(connection!, fooBarUrl, OnlineStatus.UNKNOWN, AuthenticationStatus.NO_AUTHENTICATION, 0);
|
||||
|
||||
// set prioritized connection without credentials
|
||||
await charlie.setMoneroConnection(new UriConnection()
|
||||
.setUri(TestConfig.monerod2.uri)
|
||||
await charlie.setMoneroConnection(new UrlConnection()
|
||||
.setUrl(TestConfig.monerod2.url)
|
||||
.setPriority(1));
|
||||
connection = await charlie.getMoneroConnection();
|
||||
testConnection(connection!, TestConfig.monerod2.uri, undefined, undefined, 1); // status may or may not be known due to periodic connection checking
|
||||
testConnection(connection!, TestConfig.monerod2.url, undefined, undefined, 1); // status may or may not be known due to periodic connection checking
|
||||
|
||||
// connection is offline
|
||||
connection = await charlie.checkMoneroConnection();
|
||||
testConnection(connection!, TestConfig.monerod2.uri, OnlineStatus.OFFLINE, AuthenticationStatus.NO_AUTHENTICATION, 1);
|
||||
assert(!await charlie.isConnectedToMonero());
|
||||
testConnection(connection!, TestConfig.monerod2.url, OnlineStatus.OFFLINE, AuthenticationStatus.NO_AUTHENTICATION, 1);
|
||||
|
||||
// start monerod2
|
||||
let cmd = [
|
||||
@ -280,31 +345,37 @@ test("Can manage Monero daemon connections", async () => {
|
||||
|
||||
// connection is online and not authenticated
|
||||
connection = await charlie.checkMoneroConnection();
|
||||
testConnection(connection!, TestConfig.monerod2.uri, OnlineStatus.ONLINE, AuthenticationStatus.NOT_AUTHENTICATED, 1);
|
||||
assert(!await charlie.isConnectedToMonero());
|
||||
testConnection(connection!, TestConfig.monerod2.url, OnlineStatus.ONLINE, AuthenticationStatus.NOT_AUTHENTICATED, 1);
|
||||
|
||||
// set connection credentials
|
||||
await charlie.setMoneroConnection(new UriConnection()
|
||||
.setUri(TestConfig.monerod2.uri)
|
||||
await charlie.setMoneroConnection(new UrlConnection()
|
||||
.setUrl(TestConfig.monerod2.url)
|
||||
.setUsername(TestConfig.monerod2.username)
|
||||
.setPassword(TestConfig.monerod2.password)
|
||||
.setPriority(1));
|
||||
connection = await charlie.getMoneroConnection();
|
||||
testConnection(connection!, TestConfig.monerod2.uri, undefined, undefined, 1);
|
||||
testConnection(connection!, TestConfig.monerod2.url, undefined, undefined, 1);
|
||||
|
||||
// connection is online and authenticated
|
||||
connection = await charlie.checkMoneroConnection();
|
||||
testConnection(connection!, TestConfig.monerod2.uri, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
|
||||
assert(await charlie.isConnectedToMonero());
|
||||
testConnection(connection!, TestConfig.monerod2.url, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
|
||||
|
||||
// change account password
|
||||
let password = "newPassword";
|
||||
await charlie.changePassword("newPassword");
|
||||
|
||||
// restart charlie
|
||||
let appName = charlie.getAppName();
|
||||
await stopHavenoProcess(charlie);
|
||||
charlie = await startHavenoProcess(appName, TestConfig.logging.logProcessOutput);
|
||||
await releaseHavenoProcess(charlie);
|
||||
charlie = await initHavenoDaemon({appName: appName, accountPassword: password});
|
||||
|
||||
// connection is restored, online, and authenticated
|
||||
connection = await charlie.getMoneroConnection();
|
||||
testConnection(connection!, TestConfig.monerod2.uri, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
|
||||
testConnection(connection!, TestConfig.monerod2.url, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
|
||||
connections = await charlie.getMoneroConnections();
|
||||
testConnection(getConnection(connections, monerodUri1)!, monerodUri1, OnlineStatus.UNKNOWN, AuthenticationStatus.NO_AUTHENTICATION, 1);
|
||||
testConnection(getConnection(connections, monerodUrl1)!, monerodUrl1, OnlineStatus.UNKNOWN, AuthenticationStatus.NO_AUTHENTICATION, 1);
|
||||
|
||||
// enable auto switch
|
||||
await charlie.setAutoSwitch(true);
|
||||
@ -315,58 +386,58 @@ test("Can manage Monero daemon connections", async () => {
|
||||
// test auto switch after periodic connection check
|
||||
await wait(TestConfig.daemonPollPeriodMs);
|
||||
connection = await charlie.getMoneroConnection();
|
||||
testConnection(connection!, monerodUri1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
|
||||
testConnection(connection!, monerodUrl1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
|
||||
|
||||
// stop checking connection periodically
|
||||
await charlie.stopCheckingConnection();
|
||||
|
||||
// remove current connection
|
||||
await charlie.removeMoneroConnection(monerodUri1);
|
||||
await charlie.removeMoneroConnection(monerodUrl1);
|
||||
|
||||
// check current connection
|
||||
connection = await charlie.checkMoneroConnection();
|
||||
assert.equal(undefined, connection);
|
||||
assert.equal(connection, undefined);
|
||||
|
||||
// check all connections
|
||||
await charlie.checkMoneroConnections();
|
||||
connections = await charlie.getMoneroConnections();
|
||||
testConnection(getConnection(connections, fooBarUri)!, fooBarUri, OnlineStatus.OFFLINE, AuthenticationStatus.NO_AUTHENTICATION, 0);
|
||||
for (let connection of connections) testConnection(connection!, connection.getUri(), OnlineStatus.OFFLINE, AuthenticationStatus.NO_AUTHENTICATION);
|
||||
testConnection(getConnection(connections, fooBarUrl)!, fooBarUrl, OnlineStatus.OFFLINE, AuthenticationStatus.NO_AUTHENTICATION, 0);
|
||||
for (let connection of connections) testConnection(connection!, connection.getUrl(), OnlineStatus.OFFLINE, AuthenticationStatus.NO_AUTHENTICATION);
|
||||
|
||||
// set connection to previous uri
|
||||
await charlie.setMoneroConnection(fooBarUri);
|
||||
// set connection to previous url
|
||||
await charlie.setMoneroConnection(fooBarUrl);
|
||||
connection = await charlie.getMoneroConnection();
|
||||
testConnection(connection!, fooBarUri, OnlineStatus.OFFLINE, AuthenticationStatus.NO_AUTHENTICATION, 0);
|
||||
testConnection(connection!, fooBarUrl, OnlineStatus.OFFLINE, AuthenticationStatus.NO_AUTHENTICATION, 0);
|
||||
|
||||
// set connection to new uri
|
||||
let fooBarUri2 = "http://foo.bar.xyz";
|
||||
await charlie.setMoneroConnection(fooBarUri2);
|
||||
// set connection to new url
|
||||
let fooBarUrl2 = "http://foo.bar.xyz";
|
||||
await charlie.setMoneroConnection(fooBarUrl2);
|
||||
connections = await charlie.getMoneroConnections();
|
||||
connection = getConnection(connections, fooBarUri2);
|
||||
testConnection(connection!, fooBarUri2, OnlineStatus.UNKNOWN, AuthenticationStatus.NO_AUTHENTICATION, 0);
|
||||
connection = getConnection(connections, fooBarUrl2);
|
||||
testConnection(connection!, fooBarUrl2, OnlineStatus.UNKNOWN, AuthenticationStatus.NO_AUTHENTICATION, 0);
|
||||
|
||||
// reset connection
|
||||
await charlie.setMoneroConnection();
|
||||
assert.equal(undefined, await charlie.getMoneroConnection());
|
||||
assert.equal(await charlie.getMoneroConnection(), undefined);
|
||||
|
||||
// test auto switch after start checking connection
|
||||
await charlie.setAutoSwitch(false);
|
||||
await charlie.startCheckingConnection(5000); // checks the connection
|
||||
await charlie.setAutoSwitch(true);
|
||||
await charlie.addMoneroConnection(new UriConnection()
|
||||
.setUri(TestConfig.monerod.uri)
|
||||
await charlie.addMoneroConnection(new UrlConnection()
|
||||
.setUrl(TestConfig.monerod.url)
|
||||
.setUsername(TestConfig.monerod.username)
|
||||
.setPassword(TestConfig.monerod.password)
|
||||
.setPriority(2));
|
||||
await wait(10000);
|
||||
connection = await charlie.getMoneroConnection();
|
||||
testConnection(connection!, TestConfig.monerod.uri, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 2);
|
||||
testConnection(connection!, TestConfig.monerod.url, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 2);
|
||||
} catch (err2) {
|
||||
err = err2;
|
||||
}
|
||||
|
||||
// stop processes
|
||||
if (charlie) await stopHavenoProcess(charlie);
|
||||
if (charlie) await releaseHavenoProcess(charlie);
|
||||
if (monerod2) await monerod2.stopProcess();
|
||||
// TODO: how to delete trader app folder at end of test?
|
||||
if (err) throw err;
|
||||
@ -432,6 +503,32 @@ test("Can get balances", async () => {
|
||||
expect(BigInt(balances.getReservedTradeBalance())).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test("Can receive push notifications", async () => {
|
||||
|
||||
// add notification listener
|
||||
let notifications: NotificationMessage[] = [];
|
||||
await alice.addNotificationListener(notification => {
|
||||
notifications.push(notification);
|
||||
});
|
||||
|
||||
// send test notification
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await alice._sendNotification(new NotificationMessage()
|
||||
.setTimestamp(Date.now())
|
||||
.setTitle("Test title " + i)
|
||||
.setMessage("Test message " + i));
|
||||
}
|
||||
|
||||
// test notification
|
||||
await wait(1000);
|
||||
assert(notifications.length >= 3);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
assert(notifications[i].getTimestamp() > 0);
|
||||
assert.equal(notifications[i].getTitle(), "Test title " + i);
|
||||
assert.equal(notifications[i].getMessage(), "Test message " + i);
|
||||
}
|
||||
});
|
||||
|
||||
test("Can get market prices", async () => {
|
||||
|
||||
// get all market prices
|
||||
@ -465,6 +562,27 @@ test("Can get market prices", async () => {
|
||||
.toThrow('Currency not found: INVALID_CURRENCY');
|
||||
});
|
||||
|
||||
test("Can register as dispute agents", async () => {
|
||||
await arbitrator.registerDisputeAgent("mediator", TestConfig.devPrivilegePrivKey); // TODO: bisq mediator = haveno arbitrator
|
||||
await arbitrator.registerDisputeAgent("refundagent", TestConfig.devPrivilegePrivKey); // TODO: no refund agent in haveno
|
||||
|
||||
// test bad dispute agent type
|
||||
try {
|
||||
await arbitrator.registerDisputeAgent("unsupported type", TestConfig.devPrivilegePrivKey);
|
||||
throw new Error("should have thrown error registering bad type");
|
||||
} catch (err) {
|
||||
if (err.message !== "unknown dispute agent type 'unsupported type'") throw new Error("Unexpected error: " + err.message);
|
||||
}
|
||||
|
||||
// test bad key
|
||||
try {
|
||||
await arbitrator.registerDisputeAgent("mediator", "bad key");
|
||||
throw new Error("should have thrown error registering bad key");
|
||||
} catch (err) {
|
||||
if (err.message !== "invalid registration key") throw new Error("Unexpected error: " + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
test("Can get offers", async () => {
|
||||
let offers: OfferInfo[] = await alice.getOffers("BUY");
|
||||
for (let offer of offers) {
|
||||
@ -624,7 +742,7 @@ test("Handles unexpected errors during trade initialization", async () => {
|
||||
|
||||
// start and fund 3 trader processes
|
||||
console.log("Starting trader processes");
|
||||
traders = await startHavenoProcesses(3, TestConfig.logging.logProcessOutput);
|
||||
traders = await initHavenoDaemons(3);
|
||||
console.log("Funding traders");
|
||||
let tradeAmount: bigint = BigInt("250000000000");
|
||||
await waitForUnlockedBalance(tradeAmount * BigInt("2"), traders[0], traders[1], traders[2]);
|
||||
@ -642,7 +760,7 @@ test("Handles unexpected errors during trade initialization", async () => {
|
||||
let paymentAccount = await createCryptoPaymentAccount(traders[1]);
|
||||
wait(3000).then(async function() {
|
||||
try {
|
||||
let traderWallet = await monerojs.connectToWalletRpc("http://localhost:" + traders[1].getWalletRpcPort(), "rpc_user", "abc123"); // TODO: don't hardcode here, protect wallet rpc based on account password
|
||||
let traderWallet = await monerojs.connectToWalletRpc("http://localhost:" + traders[1].getWalletRpcPort(), TestConfig.defaultHavenod.walletUsername, TestConfig.defaultHavenod.accountPassword);
|
||||
for (let frozenOutput of await traderWallet.getOutputs({isFrozen: true})) await traderWallet.thawOutput(frozenOutput.getKeyImage().getHex());
|
||||
console.log("Sweeping trade funds");
|
||||
await traderWallet.sweepUnlocked({address: await traderWallet.getPrimaryAddress(), relay: true});
|
||||
@ -676,7 +794,7 @@ test("Handles unexpected errors during trade initialization", async () => {
|
||||
// trader 0 spends trade funds then trader 2 takes offer
|
||||
wait(3000).then(async function() {
|
||||
try {
|
||||
let traderWallet = await monerojs.connectToWalletRpc("http://localhost:" + traders[0].getWalletRpcPort(), "rpc_user", "abc123"); // TODO: don't hardcode here, protect wallet rpc based on account password
|
||||
let traderWallet = await monerojs.connectToWalletRpc("http://localhost:" + traders[0].getWalletRpcPort(), TestConfig.defaultHavenod.walletUsername, TestConfig.defaultHavenod.accountPassword);
|
||||
for (let frozenOutput of await traderWallet.getOutputs({isFrozen: true})) await traderWallet.thawOutput(frozenOutput.getKeyImage().getHex());
|
||||
console.log("Sweeping offer funds");
|
||||
await traderWallet.sweepUnlocked({address: await traderWallet.getPrimaryAddress(), relay: true});
|
||||
@ -706,7 +824,7 @@ test("Handles unexpected errors during trade initialization", async () => {
|
||||
|
||||
// stop traders
|
||||
console.log("Stopping haveno processes");
|
||||
for (let trader of traders) await stopHavenoProcess(trader);
|
||||
for (let trader of traders) await releaseHavenoProcess(trader);
|
||||
if (err) throw err;
|
||||
});
|
||||
|
||||
@ -716,7 +834,7 @@ test("Cannot make or take offer with insufficient unlocked funds", async () => {
|
||||
try {
|
||||
|
||||
// start charlie
|
||||
charlie = await startHavenoProcess(undefined, TestConfig.logging.logProcessOutput);
|
||||
charlie = await initHavenoDaemon();
|
||||
|
||||
// charlie creates ethereum payment account
|
||||
let paymentAccount = await createCryptoPaymentAccount(charlie);
|
||||
@ -728,7 +846,7 @@ test("Cannot make or take offer with insufficient unlocked funds", async () => {
|
||||
} catch (err) {
|
||||
let errTyped = err as grpcWeb.RpcError;
|
||||
assert.equal(errTyped.code, 2);
|
||||
assert(errTyped.message.includes("not enough money"));
|
||||
assert(err.message.includes("not enough money"), "Unexpected error: " + err.message);
|
||||
}
|
||||
|
||||
// alice posts offer
|
||||
@ -749,7 +867,7 @@ test("Cannot make or take offer with insufficient unlocked funds", async () => {
|
||||
throw new Error("Should have failed taking offer with insufficient funds")
|
||||
} catch (err) {
|
||||
let errTyped = err as grpcWeb.RpcError;
|
||||
assert(errTyped.message.includes("not enough money"), "Unexpected error: " + errTyped.message); // TODO (woodser): error message does not contain stacktrace
|
||||
assert(errTyped.message.includes("not enough money"), "Unexpected error: " + errTyped.message);
|
||||
assert.equal(errTyped.code, 2);
|
||||
}
|
||||
|
||||
@ -766,7 +884,7 @@ test("Cannot make or take offer with insufficient unlocked funds", async () => {
|
||||
}
|
||||
|
||||
// stop charlie
|
||||
if (charlie) await stopHavenoProcess(charlie);
|
||||
if (charlie) await releaseHavenoProcess(charlie);
|
||||
// TODO: how to delete trader app folder at end of test?
|
||||
if (err) throw err;
|
||||
});
|
||||
@ -899,117 +1017,95 @@ test("Can complete a trade", async () => {
|
||||
|
||||
// ------------------------------- HELPERS ------------------------------------
|
||||
|
||||
function getConnection(connections: UriConnection[], uri: string): UriConnection | undefined {
|
||||
for (let connection of connections) if (connection.getUri() === uri) return connection;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function testConnection(connection: UriConnection, uri?: string, onlineStatus?: OnlineStatus, authenticationStatus?: AuthenticationStatus, priority?: number) {
|
||||
if (uri) assert.equal(connection.getUri(), uri);
|
||||
assert.equal(connection.getPassword(), ""); // TODO (woodser): undefined instead of ""?
|
||||
assert.equal(connection.getUsername(), "");
|
||||
if (onlineStatus !== undefined) assert.equal(connection.getOnlineStatus(), onlineStatus);
|
||||
if (authenticationStatus !== undefined) assert.equal(connection.getAuthenticationStatus(), authenticationStatus);
|
||||
if (priority !== undefined) assert.equal(connection.getPriority(), priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize arbitrator, alice, or bob by their configuration.
|
||||
*
|
||||
* @param {object} config - for arbitrator, alice, or bob
|
||||
* @return {HavenoDaemon} the created instance
|
||||
*/
|
||||
async function initHavenoDaemon(config: any): Promise<HavenoDaemon> {
|
||||
try {
|
||||
let havenod = new HavenoDaemon(config.uri, config.password);
|
||||
await havenod.getVersion();
|
||||
return havenod;
|
||||
} catch (err) {
|
||||
return startHavenoProcess(config.appName, config.logProcessOutput);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Haveno daemons as processes.
|
||||
*
|
||||
* @param {number} numProcesses - number of trader processes to start
|
||||
* @param {boolean} enableLogging - specifies if process output should be logged
|
||||
* @return {HavenoDaemon[]} clients connected to the started Haveno processes
|
||||
*/
|
||||
async function startHavenoProcesses(numProcesses: number, enableLogging: boolean): Promise<HavenoDaemon[]> {
|
||||
async function initHavenoDaemons(numDaemons: number, config?: any) {
|
||||
let traderPromises: Promise<HavenoDaemon>[] = [];
|
||||
for (let i = 0; i < numProcesses; i++) traderPromises.push(startHavenoProcess(undefined, enableLogging));
|
||||
for (let i = 0; i < numDaemons; i++) traderPromises.push(initHavenoDaemon(config));
|
||||
return Promise.all(traderPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a Haveno daemon as a process.
|
||||
*
|
||||
* If the appName belongs to the arbitrator, alice, or bob, the process is started using their configured ports.
|
||||
*
|
||||
* @param {string|undefined} appName - the app folder name (default to name with unique id)
|
||||
* @param {boolean} enableLogging - specifies if process output should be logged
|
||||
* @return {HavenoDaemon} the client connected to the started Haveno process
|
||||
*/
|
||||
async function startHavenoProcess(appName: string|undefined, enableLogging: boolean): Promise<HavenoDaemon> {
|
||||
if (!appName) appName = "haveno-XMR_STAGENET_instance_" + GenUtils.getUUID();
|
||||
async function initHavenoDaemon(config?: any): Promise<HavenoDaemon> {
|
||||
config = Object.assign({}, TestConfig.defaultHavenod, config);
|
||||
if (!config.appName) config.appName = "haveno-XMR_STAGENET_instance_" + GenUtils.getUUID();
|
||||
|
||||
// get proxy port for haveno process
|
||||
let proxyPort;
|
||||
if (appName === TestConfig.arbitrator.appName) proxyPort = "8079";
|
||||
else if (appName === TestConfig.alice.appName) proxyPort = "8080";
|
||||
else if (appName === TestConfig.bob.appName) proxyPort = "8081";
|
||||
else {
|
||||
for (let port of Array.from(TestConfig.proxyPorts.keys())) {
|
||||
if (port === "8079" || port === "8080" || port === "8081") continue; // reserved for arbitrator, alice, and bob
|
||||
if (!GenUtils.arrayContains(HAVENO_PROCESS_PORTS, port)) {
|
||||
HAVENO_PROCESS_PORTS.push(port);
|
||||
proxyPort = port;
|
||||
break;
|
||||
// connect to existing server or start new process
|
||||
let havenod;
|
||||
try {
|
||||
|
||||
// try to connect to existing server
|
||||
havenod = new HavenoDaemon(config.url, config.apiPassword);
|
||||
await havenod.getVersion();
|
||||
} catch (err) {
|
||||
|
||||
// get port for haveno process
|
||||
let proxyPort = "";
|
||||
if (config.url) proxyPort = new URL(config.url).port
|
||||
else {
|
||||
for (let port of Array.from(TestConfig.proxyPorts.keys())) {
|
||||
if (port === "8079" || port === "8080" || port === "8081") continue; // reserved for arbitrator, alice, and bob
|
||||
if (!GenUtils.arrayContains(HAVENO_PROCESS_PORTS, port)) {
|
||||
HAVENO_PROCESS_PORTS.push(port);
|
||||
proxyPort = port;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!proxyPort) throw new Error("No unused test ports available");
|
||||
|
||||
// start haveno process using configured ports if available
|
||||
let cmd: string[] = [
|
||||
"./haveno-daemon",
|
||||
"--baseCurrencyNetwork", "XMR_STAGENET",
|
||||
"--useLocalhostForP2P", "true",
|
||||
"--useDevPrivilegeKeys", "true",
|
||||
"--nodePort", TestConfig.proxyPorts.get(proxyPort)![1],
|
||||
"--appName", config.appName,
|
||||
"--apiPassword", "apitest",
|
||||
"--apiPort", TestConfig.proxyPorts.get(proxyPort)![0],
|
||||
"--walletRpcBindPort", config.walletUrl ? new URL(config.walletUrl).port : "" + await getAvailablePort(), // use configured port if given
|
||||
"--passwordRequired", (config.accountPasswordRequired ? "true" : "false")
|
||||
];
|
||||
havenod = await HavenoDaemon.startProcess(TestConfig.haveno.path, cmd, "http://localhost:" + proxyPort, config.logProcessOutput);
|
||||
HAVENO_PROCESSES.push(havenod);
|
||||
}
|
||||
if (!proxyPort) throw new Error("No unused test ports available");
|
||||
|
||||
// start haveno process using configured ports if available
|
||||
let cmd: string[] = [
|
||||
"./haveno-daemon",
|
||||
"--baseCurrencyNetwork", "XMR_STAGENET",
|
||||
"--useLocalhostForP2P", "true",
|
||||
"--useDevPrivilegeKeys", "true",
|
||||
"--nodePort", TestConfig.proxyPorts.get(proxyPort)![1],
|
||||
"--appName", appName,
|
||||
"--apiPassword", "apitest",
|
||||
"--apiPort", TestConfig.proxyPorts.get(proxyPort)![0],
|
||||
"--walletRpcBindPort", (proxyPort === "8080" ? new URL(TestConfig.alice.walletUri).port : await getFreePort()) + "" // use alice's configured wallet rpc port
|
||||
];
|
||||
let havenod = await HavenoDaemon.startProcess(TestConfig.haveno.path, cmd, "http://localhost:" + proxyPort, enableLogging);
|
||||
HAVENO_PROCESSES.push(havenod);
|
||||
// open account if configured
|
||||
if (config.autoLogin) await initHavenoAccount(havenod, config.accountPassword);
|
||||
return havenod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a free port.
|
||||
*/
|
||||
async function getFreePort(): Promise<number> {
|
||||
return new Promise(function(resolve, reject) {
|
||||
let srv = net.createServer();
|
||||
srv.listen(0, function() {
|
||||
let port = srv.address().port;
|
||||
srv.close(function() {
|
||||
resolve(port);
|
||||
})
|
||||
|
||||
async function getAvailablePort(): Promise<number> {
|
||||
return new Promise(function(resolve, reject) {
|
||||
let srv = net.createServer();
|
||||
srv.listen(0, function() {
|
||||
let port = srv.address().port;
|
||||
srv.close(function() {
|
||||
resolve(port);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a Haveno daemon process and release its ports for reuse.
|
||||
* Release a Haveno process for reuse and try to shutdown.
|
||||
*/
|
||||
async function stopHavenoProcess(havenod: HavenoDaemon) {
|
||||
await havenod.stopProcess();
|
||||
async function releaseHavenoProcess(havenod: HavenoDaemon) {
|
||||
GenUtils.remove(HAVENO_PROCESSES, havenod);
|
||||
GenUtils.remove(HAVENO_PROCESS_PORTS, new URL(havenod.getUrl()).port); // TODO (woodser): standardize to uri
|
||||
GenUtils.remove(HAVENO_PROCESS_PORTS, new URL(havenod.getUrl()).port); // TODO (woodser): standardize to url
|
||||
try {
|
||||
await havenod.shutdownServer();
|
||||
} catch (err) {
|
||||
assert.equal(err.message, OFFLINE_ERR_MSG);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or open an account with the given daemon and password.
|
||||
*/
|
||||
async function initHavenoAccount(havenod: HavenoDaemon, password: string) {
|
||||
if (await havenod.isAccountOpen()) return;
|
||||
if (await havenod.accountExists()) return havenod.openAccount(password);
|
||||
await havenod.createAccount(password);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1018,7 +1114,7 @@ async function stopHavenoProcess(havenod: HavenoDaemon) {
|
||||
async function initFundingWallet() {
|
||||
|
||||
// init client connected to monero-wallet-rpc
|
||||
fundingWallet = await monerojs.connectToWalletRpc(TestConfig.fundingWallet.uri, TestConfig.fundingWallet.username, TestConfig.fundingWallet.password);
|
||||
fundingWallet = await monerojs.connectToWalletRpc(TestConfig.fundingWallet.url, TestConfig.fundingWallet.username, TestConfig.fundingWallet.password);
|
||||
|
||||
// check if wallet is open
|
||||
let walletIsOpen = false
|
||||
@ -1164,6 +1260,20 @@ function getNotifications(notifications: NotificationMessage[], notificationType
|
||||
return filteredNotifications;
|
||||
}
|
||||
|
||||
function getConnection(connections: UrlConnection[], url: string): UrlConnection | undefined {
|
||||
for (let connection of connections) if (connection.getUrl() === url) return connection;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function testConnection(connection: UrlConnection, url?: string, onlineStatus?: OnlineStatus, authenticationStatus?: AuthenticationStatus, priority?: number) {
|
||||
if (url) assert.equal(connection.getUrl(), url);
|
||||
assert.equal(connection.getPassword(), ""); // TODO (woodser): undefined instead of ""?
|
||||
assert.equal(connection.getUsername(), "");
|
||||
if (onlineStatus !== undefined) assert.equal(connection.getOnlineStatus(), onlineStatus);
|
||||
if (authenticationStatus !== undefined) assert.equal(connection.getAuthenticationStatus(), authenticationStatus);
|
||||
if (priority !== undefined) assert.equal(connection.getPriority(), priority);
|
||||
}
|
||||
|
||||
function testTx(tx: XmrTx, ctx: TxContext) {
|
||||
assert(tx.getHash());
|
||||
expect(BigInt(tx.getFee())).toBeLessThan(TestConfig.maxFee);
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {HavenoUtils} from "./utils/HavenoUtils";
|
||||
import {TaskLooper} from "./utils/TaskLooper";
|
||||
import * as grpcWeb from 'grpc-web';
|
||||
import {DisputeAgentsClient, GetVersionClient, NotificationsClient, PriceClient, WalletsClient, OffersClient, PaymentAccountsClient, TradesClient, MoneroConnectionsClient} from './protobuf/GrpcServiceClientPb';
|
||||
import {CancelOfferRequest, ConfirmPaymentReceivedRequest, ConfirmPaymentStartedRequest, CreateCryptoCurrencyPaymentAccountReply, CreateCryptoCurrencyPaymentAccountRequest, CreateOfferReply, CreateOfferRequest, CreateXmrTxReply, CreateXmrTxRequest, GetBalancesReply, GetBalancesRequest, GetNewDepositSubaddressReply, GetNewDepositSubaddressRequest, GetOffersReply, GetOffersRequest, GetPaymentAccountsReply, GetPaymentAccountsRequest, GetTradeReply, GetTradeRequest, GetTradesReply, GetTradesRequest, GetVersionReply, GetVersionRequest, GetXmrTxsReply, GetXmrTxsRequest, MarketPriceInfo, MarketPriceReply, MarketPriceRequest, MarketPricesReply, MarketPricesRequest, NotificationMessage, OfferInfo, RegisterDisputeAgentRequest, RegisterNotificationListenerRequest, RelayXmrTxReply, RelayXmrTxRequest, SendNotificationRequest, TakeOfferReply, TakeOfferRequest, TradeInfo, XmrBalanceInfo, XmrDestination, XmrTx, UriConnection, AddConnectionRequest, RemoveConnectionRequest, GetConnectionRequest, GetConnectionsRequest, SetConnectionRequest, CheckConnectionRequest, CheckConnectionsReply, CheckConnectionsRequest, StartCheckingConnectionsRequest, StopCheckingConnectionsRequest, GetBestAvailableConnectionRequest, SetAutoSwitchRequest, CheckConnectionReply, GetConnectionsReply, GetConnectionReply, GetBestAvailableConnectionReply} from './protobuf/grpc_pb';
|
||||
import {AvailabilityResult, PaymentAccount} from './protobuf/pb_pb';
|
||||
import {GetVersionClient, AccountClient, MoneroConnectionsClient, DisputeAgentsClient, NotificationsClient, WalletsClient, PriceClient, OffersClient, PaymentAccountsClient, TradesClient, ShutdownServerClient} from './protobuf/GrpcServiceClientPb';
|
||||
import {GetVersionRequest, GetVersionReply, IsAppInitializedRequest, IsAppInitializedReply, RegisterDisputeAgentRequest, MarketPriceRequest, MarketPriceReply, MarketPricesRequest, MarketPricesReply, MarketPriceInfo, GetBalancesRequest, GetBalancesReply, XmrBalanceInfo, GetOffersRequest, GetOffersReply, OfferInfo, 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} from './protobuf/grpc_pb';
|
||||
import {PaymentAccount, AvailabilityResult} from './protobuf/pb_pb';
|
||||
const console = require('console');
|
||||
|
||||
/**
|
||||
@ -12,6 +12,7 @@ const console = require('console');
|
||||
class HavenoDaemon {
|
||||
|
||||
// grpc clients
|
||||
_appName: string|undefined;
|
||||
_getVersionClient: GetVersionClient;
|
||||
_disputeAgentsClient: DisputeAgentsClient;
|
||||
_notificationsClient: NotificationsClient;
|
||||
@ -21,17 +22,24 @@ class HavenoDaemon {
|
||||
_paymentAccountsClient: PaymentAccountsClient;
|
||||
_offersClient: OffersClient;
|
||||
_tradesClient: TradesClient;
|
||||
_accountClient: AccountClient;
|
||||
_shutdownServerClient: ShutdownServerClient;
|
||||
|
||||
// other instance variables
|
||||
// state variables
|
||||
_url: string;
|
||||
_password: string;
|
||||
_process: any;
|
||||
_processLogging: boolean = false;
|
||||
_processLogging = false;
|
||||
_walletRpcPort: number|undefined;
|
||||
_notificationListeners: ((notification: NotificationMessage) => void)[] = [];
|
||||
_registerNotificationListenerCalled = false;
|
||||
_keepAliveLooper: any;
|
||||
_keepAlivePeriodMs: number = 60000;
|
||||
_appName: string|undefined;
|
||||
|
||||
|
||||
// constants
|
||||
static readonly _fullyInitializedMessage = "AppStartupState: Application fully initialized";
|
||||
static readonly _loginRequiredMessage = "HavenoDaemonMain: Interactive login required";
|
||||
|
||||
/**
|
||||
* Construct a client connected to a Haveno daemon.
|
||||
*
|
||||
@ -39,20 +47,22 @@ class HavenoDaemon {
|
||||
* @param {string} password - Haveno daemon password
|
||||
*/
|
||||
constructor(url: string, password: string) {
|
||||
HavenoUtils.log(2, "Creating HavenoDaemon(" + url + ", " + password + ")");
|
||||
if (!url) throw new Error("Must provide URL of Haveno daemon");
|
||||
if (!password) throw new Error("Must provide password of Haveno daemon");
|
||||
HavenoUtils.log(2, "Creating HavenoDaemon(" + url + ", " + password + ")");
|
||||
this._url = url;
|
||||
this._password = password;
|
||||
this._getVersionClient = new GetVersionClient(this._url);
|
||||
this._disputeAgentsClient = new DisputeAgentsClient(this._url);
|
||||
this._accountClient = new AccountClient(this._url);
|
||||
this._moneroConnectionsClient = new MoneroConnectionsClient(this._url)
|
||||
this._disputeAgentsClient = new DisputeAgentsClient(this._url);
|
||||
this._walletsClient = new WalletsClient(this._url);
|
||||
this._priceClient = new PriceClient(this._url);
|
||||
this._paymentAccountsClient = new PaymentAccountsClient(this._url);
|
||||
this._offersClient = new OffersClient(this._url);
|
||||
this._tradesClient = new TradesClient(this._url);
|
||||
this._notificationsClient = new NotificationsClient(this._url);
|
||||
this._shutdownServerClient = new ShutdownServerClient(this._url);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,7 +82,7 @@ class HavenoDaemon {
|
||||
|
||||
// state variables
|
||||
let output = "";
|
||||
let isResolved = false;
|
||||
let isStarted = false;
|
||||
let daemon: HavenoDaemon|undefined = undefined;
|
||||
|
||||
// start process
|
||||
@ -83,11 +93,11 @@ class HavenoDaemon {
|
||||
// handle stdout
|
||||
childProcess.stdout.on('data', async function(data: any) {
|
||||
let line = data.toString();
|
||||
if (HavenoUtils.getLogLevel() >= 3 && loggingEnabled()) process.stdout.write(line);
|
||||
if (loggingEnabled()) process.stdout.write(line);
|
||||
output += line + '\n'; // capture output in case of error
|
||||
|
||||
// read success message
|
||||
if (line.indexOf("HavenoHeadlessAppMain: onSetupComplete") >= 0) {
|
||||
// initialize daemon on success or login required message
|
||||
if (!daemon && (line.indexOf(HavenoDaemon._fullyInitializedMessage) >= 0 || line.indexOf(HavenoDaemon._loginRequiredMessage) >= 0)) {
|
||||
|
||||
// get api password
|
||||
let passwordIdx = cmd.indexOf("--apiPassword");
|
||||
@ -108,40 +118,40 @@ class HavenoDaemon {
|
||||
if (walletRpcPortIdx >= 0) daemon._walletRpcPort = parseInt(cmd[walletRpcPortIdx + 1]);
|
||||
|
||||
// resolve promise with client connected to internal process
|
||||
isResolved = true;
|
||||
isStarted = true;
|
||||
resolve(daemon);
|
||||
}
|
||||
|
||||
// read error message
|
||||
if (line.indexOf("[HavenoDaemonMain] ERROR") >= 0) {
|
||||
if (!isResolved) await rejectProcess(new Error(line));
|
||||
if (!isStarted) await rejectStartup(new Error(line));
|
||||
}
|
||||
});
|
||||
|
||||
// handle stderr
|
||||
childProcess.stderr.on('data', function(data: any) {
|
||||
if (HavenoUtils.getLogLevel() >= 2 && loggingEnabled()) process.stderr.write(data);
|
||||
if (loggingEnabled()) process.stderr.write(data);
|
||||
});
|
||||
|
||||
// handle exit
|
||||
childProcess.on("exit", async function(code: any) {
|
||||
if (!isResolved) await rejectProcess(new Error("Haveno process terminated with exit code " + code + (output ? ":\n\n" + output : "")));
|
||||
if (!isStarted) await rejectStartup(new Error("Haveno process terminated with exit code " + code + (output ? ":\n\n" + output : "")));
|
||||
});
|
||||
|
||||
// handle error
|
||||
childProcess.on("error", async function(err: any) {
|
||||
if (err.message.indexOf("ENOENT") >= 0) reject(new Error("haveno-daemon does not exist at path '" + cmd[0] + "'"));
|
||||
if (!isResolved) await rejectProcess(err);
|
||||
if (!isStarted) await rejectStartup(err);
|
||||
});
|
||||
|
||||
// handle uncaught exception
|
||||
childProcess.on("uncaughtException", async function(err: any, origin: any) {
|
||||
console.error("Uncaught exception in Haveno process: " + err.message);
|
||||
console.error(origin);
|
||||
await rejectProcess(err);
|
||||
await rejectStartup(err);
|
||||
});
|
||||
|
||||
async function rejectProcess(err: any) {
|
||||
async function rejectStartup(err: any) {
|
||||
await HavenoUtils.kill(childProcess);
|
||||
reject(err);
|
||||
}
|
||||
@ -152,14 +162,6 @@ class HavenoDaemon {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a previously started Haveno process.
|
||||
*/
|
||||
async stopProcess(): Promise<void> {
|
||||
if (this._process === undefined) throw new Error("HavenoDaemon instance not created from new process");
|
||||
return HavenoUtils.kill(this._process);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the process running the haveno daemon.
|
||||
*
|
||||
@ -220,18 +222,90 @@ class HavenoDaemon {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register as a dispute agent.
|
||||
* Indicates if connected and authenticated with the Haveno daemon.
|
||||
*
|
||||
* @param {string} disputeAgentType - type of dispute agent to register, e.g. mediator, refundagent
|
||||
* @param {string} registrationKey - registration key
|
||||
* @return {boolean} true if connected with the Haveno daemon, false otherwise
|
||||
*/
|
||||
async registerDisputeAgent(disputeAgentType: string, registrationKey: string): Promise<void> {
|
||||
async isConnectedToDaemon(): Promise<boolean> {
|
||||
try {
|
||||
await this.getVersion();
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the Haveno account is created.
|
||||
*
|
||||
* @return {boolean} true if the account is created, false otherwise
|
||||
*/
|
||||
async accountExists(): Promise<boolean> {
|
||||
let that = this;
|
||||
let request = new RegisterDisputeAgentRequest()
|
||||
.setDisputeAgentType(disputeAgentType)
|
||||
.setRegistrationKey(registrationKey);
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._disputeAgentsClient.registerDisputeAgent(request, {password: that._password}, function(err: grpcWeb.RpcError) {
|
||||
that._accountClient.accountExists(new AccountExistsRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: AccountExistsReply) {
|
||||
if (err) reject(err);
|
||||
else resolve(response.getAccountExists());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the Haveno account is open and authenticated with the correct password.
|
||||
*
|
||||
* @return {boolean} true if the account is open and authenticated, false otherwise
|
||||
*/
|
||||
async isAccountOpen(): Promise<boolean> {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._accountClient.isAccountOpen(new IsAccountOpenRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: IsAccountOpenReply) {
|
||||
if (err) reject(err);
|
||||
else resolve(response.getIsAccountOpen());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and open a new Haveno account.
|
||||
*
|
||||
* @param {string} password - the password to encrypt the account
|
||||
*/
|
||||
async createAccount(password: string): Promise<void> {
|
||||
let that = this;
|
||||
await new Promise(function(resolve, reject) {
|
||||
that._accountClient.createAccount(new CreateAccountRequest().setPassword(password), {password: that._password}, function(err: grpcWeb.RpcError) {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
return this._awaitAppInitialized(); // TODO: grpc should not return before setup is complete
|
||||
}
|
||||
|
||||
/**
|
||||
* Open existing Haveno account.
|
||||
*
|
||||
* @param {string} password - the account password
|
||||
*/
|
||||
async openAccount(password: string): Promise<void> {
|
||||
let that = this;
|
||||
await new Promise(function(resolve, reject) {
|
||||
that._accountClient.openAccount(new OpenAccountRequest().setPassword(password), {password: that._password}, function(err: grpcWeb.RpcError) {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
return this._awaitAppInitialized(); // TODO: grpc should not return before setup is complete
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the Haveno account password.
|
||||
*
|
||||
* @param {string} password - the new account password
|
||||
*/
|
||||
async changePassword(password: string): Promise<void> {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._accountClient.changePassword(new ChangePasswordRequest().setPassword(password), {password: that._password}, function(err: grpcWeb.RpcError) {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
@ -239,24 +313,120 @@ class HavenoDaemon {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener to receive notifications from the Haveno daemon.
|
||||
* Close the currently open account.
|
||||
*/
|
||||
async closeAccount(): Promise<void> {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._accountClient.closeAccount(new CloseAccountRequest(), {password: that._password}, function(err: grpcWeb.RpcError) {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently delete the Haveno account and shutdown the server. // TODO: possible to not shutdown server?
|
||||
*/
|
||||
async deleteAccount(): Promise<void> {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._accountClient.deleteAccount(new DeleteAccountRequest(), {password: that._password}, async function(err: grpcWeb.RpcError) {
|
||||
if (err) reject(err);
|
||||
else setTimeout(resolve, 5000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup the account to the given stream. TODO: stream type?
|
||||
*/
|
||||
async backupAccount(stream: any): Promise<number> {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
let total = 0;
|
||||
let response = that._accountClient.backupAccount(new BackupAccountRequest(), {password: that._password});
|
||||
response.on('data', (chunk) => {
|
||||
let bytes = (chunk as BackupAccountReply).getZipBytes(); // TODO: right api?
|
||||
total += bytes.length;
|
||||
stream.write(bytes);
|
||||
});
|
||||
response.on('error', function(err) {
|
||||
if(err) reject(err);
|
||||
});
|
||||
response.on('end', function() {
|
||||
resolve(total);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the account from zip bytes.
|
||||
*
|
||||
* Sends chunked requests if size over max grpc envelope size (41943404 bytes).
|
||||
*
|
||||
* @param {Uint8Array} zipBytes - the bytes of the zipped account to restore
|
||||
*/
|
||||
async restoreAccount(zipBytes: Uint8Array): Promise<void> {
|
||||
if (zipBytes.length === 0) throw new Error("Zip bytes must not be empty")
|
||||
let totalLength = zipBytes.byteLength;
|
||||
let offset = 0;
|
||||
let chunkSize = 4000000; // the max frame size is 4194304 but leave room for http headers
|
||||
let hasMore = true;
|
||||
while (true) {
|
||||
if (zipBytes.byteLength <= offset + 1) return;
|
||||
if (zipBytes.byteLength <= offset + chunkSize) {
|
||||
chunkSize = zipBytes.byteLength - offset - 1;
|
||||
hasMore = false;
|
||||
}
|
||||
let subArray = zipBytes.subarray(offset, offset + chunkSize);
|
||||
await this._restoreAccountChunk(subArray, offset, totalLength, hasMore);
|
||||
offset += chunkSize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener to receive notifications from the Haveno daemon.
|
||||
*
|
||||
* @param {HavenoDaemonListener} listener - the notification listener to add
|
||||
*/
|
||||
async addNotificationListener(listener: (notification: NotificationMessage) => void): Promise<void> {
|
||||
this._notificationListeners.push(listener);
|
||||
if (this._notificationListeners.length === 1) return this._registerNotificationListener();
|
||||
return this._registerNotificationListenerOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a notification listener.
|
||||
*
|
||||
* @param {HavenoDaemonListener} listener - the notification listener to remove
|
||||
*/
|
||||
async removeNotificationListener(listener: (notification: NotificationMessage) => void): Promise<void> {
|
||||
let idx = this._notificationListeners.indexOf(listener);
|
||||
if (idx > -1) this._notificationListeners.splice(idx, 1);
|
||||
else throw new Error("Notification listener is not registered");
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if connected to the Monero network based on last connection check.
|
||||
*
|
||||
* @return {boolean} true if connected to the Monero network, false otherwise
|
||||
*/
|
||||
async isConnectedToMonero(): Promise<boolean> {
|
||||
let connection = await this.getMoneroConnection();
|
||||
return connection !== undefined &&
|
||||
connection.getOnlineStatus()! === UrlConnection.OnlineStatus.ONLINE &&
|
||||
connection.getAuthenticationStatus()! !== UrlConnection.AuthenticationStatus.NOT_AUTHENTICATED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Monero daemon connection.
|
||||
*
|
||||
* @param {string | UriConnection} connection - daemon uri or connection to add
|
||||
* @param {string | UrlConnection} connection - daemon url or connection to add
|
||||
*/
|
||||
async addMoneroConnection(connection: string | UriConnection): Promise<void> {
|
||||
async addMoneroConnection(connection: string | UrlConnection): Promise<void> {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._moneroConnectionsClient.addConnection(new AddConnectionRequest().setConnection(typeof connection === "string" ? new UriConnection().setUri(connection) : connection), {password: that._password}, function(err: grpcWeb.RpcError) {
|
||||
that._moneroConnectionsClient.addConnection(new AddConnectionRequest().setConnection(typeof connection === "string" ? new UrlConnection().setUrl(connection) : connection), {password: that._password}, function(err: grpcWeb.RpcError) {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
@ -266,12 +436,12 @@ class HavenoDaemon {
|
||||
/**
|
||||
* Remove a Monero daemon connection.
|
||||
*
|
||||
* @param {string} uri - uri of the daemon connection to remove
|
||||
* @param {string} url - url of the daemon connection to remove
|
||||
*/
|
||||
async removeMoneroConnection(uri: string): Promise<void> {
|
||||
async removeMoneroConnection(url: string): Promise<void> {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._moneroConnectionsClient.removeConnection(new RemoveConnectionRequest().setUri(uri), {password: that._password}, function(err: grpcWeb.RpcError) {
|
||||
that._moneroConnectionsClient.removeConnection(new RemoveConnectionRequest().setUrl(url), {password: that._password}, function(err: grpcWeb.RpcError) {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
@ -281,9 +451,9 @@ class HavenoDaemon {
|
||||
/**
|
||||
* Get the current Monero daemon connection.
|
||||
*
|
||||
* @return {UriConnection | undefined} the current daemon connection, undefined if no current connection
|
||||
* @return {UrlConnection | undefined} the current daemon connection, undefined if no current connection
|
||||
*/
|
||||
async getMoneroConnection(): Promise<UriConnection | undefined> {
|
||||
async getMoneroConnection(): Promise<UrlConnection | undefined> {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._moneroConnectionsClient.getConnection(new GetConnectionRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: GetConnectionReply) {
|
||||
@ -296,9 +466,9 @@ class HavenoDaemon {
|
||||
/**
|
||||
* Get all Monero daemon connections.
|
||||
*
|
||||
* @return {UriConnection[]} all daemon connections
|
||||
* @return {UrlConnection[]} all daemon connections
|
||||
*/
|
||||
async getMoneroConnections(): Promise<UriConnection[]> {
|
||||
async getMoneroConnections(): Promise<UrlConnection[]> {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._moneroConnectionsClient.getConnections(new GetConnectionsRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: GetConnectionsReply) {
|
||||
@ -310,18 +480,18 @@ class HavenoDaemon {
|
||||
|
||||
/**
|
||||
* Set the current Monero daemon connection.
|
||||
*
|
||||
*
|
||||
* Add the connection if not previously seen.
|
||||
* If the connection is provided as string, connect to the URI with any previously set credentials and priority.
|
||||
* If the connection is provided as UriConnection, overwrite any previously set credentials and priority.
|
||||
* If the connection is provided as UrlConnection, overwrite any previously set credentials and priority.
|
||||
* If undefined connection provided, disconnect the client.
|
||||
*
|
||||
* @param {string | UriConnection} connection - connection to set as current
|
||||
* @param {string | UrlConnection} connection - connection to set as current
|
||||
*/
|
||||
async setMoneroConnection(connection?: string | UriConnection): Promise<void> {
|
||||
async setMoneroConnection(connection?: string | UrlConnection): Promise<void> {
|
||||
let that = this;
|
||||
let request = new SetConnectionRequest();
|
||||
if (typeof connection === "string") request.setUri(connection);
|
||||
if (typeof connection === "string") request.setUrl(connection);
|
||||
else request.setConnection(connection);
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._moneroConnectionsClient.setConnection(request, {password: that._password}, function(err: grpcWeb.RpcError) {
|
||||
@ -336,9 +506,9 @@ class HavenoDaemon {
|
||||
*
|
||||
* If disconnected and auto switch enabled, switch to the best available connection and return its status.
|
||||
*
|
||||
* @return {UriConnection | undefined} the current daemon connection status, undefined if no current connection
|
||||
* @return {UrlConnection | undefined} the current daemon connection status, undefined if no current connection
|
||||
*/
|
||||
async checkMoneroConnection(): Promise<UriConnection | undefined> {
|
||||
async checkMoneroConnection(): Promise<UrlConnection | undefined> {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._moneroConnectionsClient.checkConnection(new CheckConnectionRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: CheckConnectionReply) {
|
||||
@ -351,9 +521,9 @@ class HavenoDaemon {
|
||||
/**
|
||||
* Check all Monero daemon connections.
|
||||
*
|
||||
* @return {UriConnection[]} status of all managed connections.
|
||||
* @return {UrlConnection[]} status of all managed connections.
|
||||
*/
|
||||
async checkMoneroConnections(): Promise<UriConnection[]> {
|
||||
async checkMoneroConnections(): Promise<UrlConnection[]> {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._moneroConnectionsClient.checkConnections(new CheckConnectionsRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: CheckConnectionsReply) {
|
||||
@ -394,9 +564,9 @@ class HavenoDaemon {
|
||||
/**
|
||||
* Get the best available connection in order of priority then response time.
|
||||
*
|
||||
* @return {UriConnection | undefined} the best available connection in order of priority then response time, undefined if no connections available
|
||||
* @return {UrlConnection | undefined} the best available connection in order of priority then response time, undefined if no connections available
|
||||
*/
|
||||
async getBestAvailableConnection(): Promise<UriConnection | undefined> {
|
||||
async getBestAvailableConnection(): Promise<UrlConnection | undefined> {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._moneroConnectionsClient.getBestAvailableConnection(new GetBestAvailableConnectionRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: GetBestAvailableConnectionReply) {
|
||||
@ -420,7 +590,26 @@ class HavenoDaemon {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register as a dispute agent.
|
||||
*
|
||||
* @param {string} disputeAgentType - type of dispute agent to register, e.g. mediator, refundagent
|
||||
* @param {string} registrationKey - registration key
|
||||
*/
|
||||
async registerDisputeAgent(disputeAgentType: string, registrationKey: string): Promise<void> {
|
||||
let that = this;
|
||||
let request = new RegisterDisputeAgentRequest()
|
||||
.setDisputeAgentType(disputeAgentType)
|
||||
.setRegistrationKey(registrationKey);
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._disputeAgentsClient.registerDisputeAgent(request, {password: that._password}, function(err: grpcWeb.RpcError) {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's balances.
|
||||
*
|
||||
@ -770,28 +959,87 @@ class HavenoDaemon {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the Haveno daemon server and stop the process if applicable.
|
||||
*/
|
||||
async shutdownServer() {
|
||||
if (this._keepAliveLooper) this._keepAliveLooper.stop();
|
||||
let that = this;
|
||||
await new Promise(function(resolve, reject) {
|
||||
that._shutdownServerClient.stop(new StopRequest(), {password: that._password}, function(err: grpcWeb.RpcError) { // process receives 'exit' event
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
if (this._process) return HavenoUtils.kill(this._process);
|
||||
}
|
||||
|
||||
// ------------------------------- HELPERS ----------------------------------
|
||||
|
||||
/**
|
||||
* Wait for the application to be fully initialized with an account and a
|
||||
* connection to the Haveno network.
|
||||
*
|
||||
* TODO:
|
||||
*
|
||||
* Currently when the application starts, the account is first initialized with createAccount()
|
||||
* or openAccount() which return immediately. A notification is sent after all setup is complete and
|
||||
* the application is connected to the Haveno network.
|
||||
*
|
||||
* Ideally when the application starts, the system checks the Haveno network connection, supporting
|
||||
* havenod.isHavenoConnectionInitialized() and havenod.awaitHavenoConnectionInitialized().
|
||||
* Independently, gRPC createAccount() and openAccount() return after all account setup and reading from disk.
|
||||
*/
|
||||
async _awaitAppInitialized(): Promise<void> {
|
||||
let that = this;
|
||||
return new Promise(async function(resolve) {
|
||||
let isResolved = false;
|
||||
let listener = async function(notification: NotificationMessage) {
|
||||
if (notification.getType() === NotificationMessage.NotificationType.APP_INITIALIZED) await resolveOnce();
|
||||
}
|
||||
await that.addNotificationListener(listener);
|
||||
if (await that._isAppInitialized()) await resolveOnce();
|
||||
async function resolveOnce() {
|
||||
if (isResolved) return;
|
||||
isResolved = true;
|
||||
await that.removeNotificationListener(listener);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async _isAppInitialized(): Promise<boolean> {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._accountClient.isAppInitialized(new IsAppInitializedRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: IsAppInitializedReply) {
|
||||
if (err) reject(err);
|
||||
else resolve(response.getIsAppInitialized());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a listener to receive notifications.
|
||||
* Due to the nature of grpc streaming, this method returns a promise
|
||||
* which may be resolved before the listener is actually registered.
|
||||
*/
|
||||
async _registerNotificationListener(): Promise<void> {
|
||||
async _registerNotificationListenerOnce(): Promise<void> {
|
||||
if (this._registerNotificationListenerCalled) return;
|
||||
else this._registerNotificationListenerCalled = true;
|
||||
let that = this;
|
||||
return new Promise(function(resolve) {
|
||||
|
||||
|
||||
// send request to register client listener
|
||||
that._notificationsClient.registerNotificationListener(new RegisterNotificationListenerRequest(), {password: that._password})
|
||||
.on("data", (data) => {
|
||||
.on('data', (data) => {
|
||||
if (data instanceof NotificationMessage) {
|
||||
for (let listener of that._notificationListeners) listener(data);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// periodically send keep alive requests // TODO (woodser): better way to keep notification stream alive?
|
||||
let firstRequest = true;
|
||||
let taskLooper = new TaskLooper(async function() {
|
||||
that._keepAliveLooper = new TaskLooper(async function() {
|
||||
if (firstRequest) {
|
||||
firstRequest = false;
|
||||
return;
|
||||
@ -800,16 +1048,15 @@ class HavenoDaemon {
|
||||
.setType(NotificationMessage.NotificationType.KEEP_ALIVE)
|
||||
.setTimestamp(Date.now()));
|
||||
});
|
||||
taskLooper.start(that._keepAlivePeriodMs);
|
||||
|
||||
// TODO: call returns before listener registered
|
||||
setTimeout(function() { resolve(); }, 1000);
|
||||
that._keepAliveLooper.start(that._keepAlivePeriodMs);
|
||||
|
||||
setTimeout(resolve, 1000); // TODO: call returns before listener registered
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send a notification.
|
||||
*
|
||||
*
|
||||
* @param {NotificationMessage} notification - notification to send
|
||||
*/
|
||||
async _sendNotification(notification: NotificationMessage): Promise<void> {
|
||||
@ -821,6 +1068,24 @@ class HavenoDaemon {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore an account chunk from zip bytes.
|
||||
*/
|
||||
async _restoreAccountChunk(zipBytes: Uint8Array, offset: number, totalLength: number, hasMore: boolean): Promise<void> {
|
||||
let that = this;
|
||||
let request = new RestoreAccountRequest()
|
||||
.setZipBytes(zipBytes)
|
||||
.setOffset(offset)
|
||||
.setTotalLength(totalLength)
|
||||
.setHasMore(hasMore);
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._accountClient.restoreAccount(request, {password: that._password}, function(err: grpcWeb.RpcError) {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {HavenoDaemon};
|
||||
|
@ -43,13 +43,14 @@ class HavenoUtils {
|
||||
*
|
||||
* TODO (woodser): move this to monero-javascript GenUtils.js as common utility
|
||||
*
|
||||
* @param process is the nodejs child process to child
|
||||
* @param {Process} process - the nodejs child process to child
|
||||
* @param {String} signal - the kill signal, e.g. SIGTERM, SIGKILL, SIGINT (default)
|
||||
*/
|
||||
static async kill(process: any): Promise<void> {
|
||||
static async kill(process: any, signal?: string): Promise<void> {
|
||||
return new Promise(function(resolve, reject) {
|
||||
process.on("exit", function() { resolve(); });
|
||||
process.on("error", function(err: any) { reject(err); });
|
||||
process.kill("SIGINT");
|
||||
process.kill(signal ? signal : "SIGINT");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user