mirror of
https://github.com/haveno-dex/haveno-ts.git
synced 2025-01-26 06:26:03 -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
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
backup.zip
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
@ -17,7 +18,6 @@
|
|||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
@ -4,11 +4,11 @@
|
|||||||
import {HavenoDaemon} from "./HavenoDaemon";
|
import {HavenoDaemon} from "./HavenoDaemon";
|
||||||
import {HavenoUtils} from "./utils/HavenoUtils";
|
import {HavenoUtils} from "./utils/HavenoUtils";
|
||||||
import * as grpcWeb from 'grpc-web';
|
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 {PaymentAccount} from './protobuf/pb_pb';
|
||||||
import {XmrDestination, XmrTx, XmrIncomingTransfer, XmrOutgoingTransfer} from './protobuf/grpc_pb';
|
import {XmrDestination, XmrTx, XmrIncomingTransfer, XmrOutgoingTransfer} from './protobuf/grpc_pb';
|
||||||
import AuthenticationStatus = UriConnection.AuthenticationStatus;
|
import AuthenticationStatus = UrlConnection.AuthenticationStatus;
|
||||||
import OnlineStatus = UriConnection.OnlineStatus;
|
import OnlineStatus = UrlConnection.OnlineStatus;
|
||||||
|
|
||||||
// import monero-javascript
|
// import monero-javascript
|
||||||
const monerojs = require("monero-javascript"); // TODO (woodser): support typescript and `npm install @types/monero-javascript` in 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;
|
const TaskLooper = monerojs.TaskLooper;
|
||||||
|
|
||||||
// other required imports
|
// other required imports
|
||||||
const console = require('console'); // import console because jest swallows messages in real time
|
const fs = require('fs');
|
||||||
const assert = require("assert");
|
|
||||||
const net = require('net');
|
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 ---------------------------------
|
||||||
|
|
||||||
// test config
|
|
||||||
const TestConfig = {
|
const TestConfig = {
|
||||||
logging: {
|
logLevel: 0,
|
||||||
level: 0, // set log level (gets more verbose increasing from 0)
|
|
||||||
logProcessOutput: false // enable or disable logging process output
|
|
||||||
},
|
|
||||||
moneroBinsDir: "../haveno/.localnet",
|
moneroBinsDir: "../haveno/.localnet",
|
||||||
networkType: monerojs.MoneroNetworkType.STAGENET,
|
networkType: monerojs.MoneroNetworkType.STAGENET,
|
||||||
haveno: {
|
haveno: {
|
||||||
@ -38,45 +35,52 @@ const TestConfig = {
|
|||||||
version: "1.6.2"
|
version: "1.6.2"
|
||||||
},
|
},
|
||||||
monerod: {
|
monerod: {
|
||||||
uri: "http://localhost:38081",
|
url: "http://localhost:38081",
|
||||||
username: "superuser",
|
username: "superuser",
|
||||||
password: "abctesting123"
|
password: "abctesting123"
|
||||||
},
|
},
|
||||||
monerod2: {
|
monerod2: {
|
||||||
uri: "http://localhost:58081",
|
url: "http://localhost:58081",
|
||||||
username: "superuser",
|
username: "superuser",
|
||||||
password: "abctesting123"
|
password: "abctesting123"
|
||||||
},
|
},
|
||||||
fundingWallet: {
|
fundingWallet: {
|
||||||
uri: "http://localhost:38084",
|
url: "http://localhost:38084",
|
||||||
username: "rpc_user",
|
username: "rpc_user",
|
||||||
password: "abc123",
|
password: "abc123",
|
||||||
defaultPath: "test_funding_wallet",
|
defaultPath: "test_funding_wallet",
|
||||||
minimumFunding: BigInt("5000000000000")
|
minimumFunding: BigInt("5000000000000")
|
||||||
},
|
},
|
||||||
arbitrator: {
|
defaultHavenod: {
|
||||||
logProcessOutput: false,
|
logProcessOutput: false, // log output for processes started by tests (except arbitrator, alice, and bob which are configured separately)
|
||||||
appName: "haveno-XMR_STAGENET_arbitrator",
|
apiPassword: "apitest",
|
||||||
uri: "http://localhost:8079",
|
walletUsername: "haveno_user",
|
||||||
password: "apitest",
|
walletDefaultPassword: "password", // only used if account password not set
|
||||||
walletUsername: "rpc_user",
|
accountPasswordRequired: true,
|
||||||
walletPassword: "abc123"
|
accountPassword: "abctesting789",
|
||||||
|
autoLogin: true
|
||||||
},
|
},
|
||||||
alice: {
|
startupHavenods: [{
|
||||||
logProcessOutput: false,
|
appName: "haveno-XMR_STAGENET_arbitrator", // arbritrator
|
||||||
appName: "haveno-XMR_STAGENET_alice",
|
logProcessOutput: false,
|
||||||
uri: "http://localhost:8080",
|
url: "http://localhost:8079",
|
||||||
password: "apitest",
|
accountPasswordRequired: false,
|
||||||
walletUri: "http://127.0.0.1:38091",
|
accountPassword: "abctesting123",
|
||||||
walletUsername: "rpc_user",
|
}, {
|
||||||
walletPassword: "abc123"
|
appName: "haveno-XMR_STAGENET_alice", // alice
|
||||||
},
|
logProcessOutput: false,
|
||||||
bob: {
|
url: "http://localhost:8080",
|
||||||
logProcessOutput: false,
|
accountPasswordRequired: false,
|
||||||
appName: "haveno-XMR_STAGENET_bob",
|
accountPassword: "abctesting456",
|
||||||
uri: "http://localhost:8081",
|
walletUrl: "http://127.0.0.1:38091",
|
||||||
password: "apitest",
|
}, {
|
||||||
},
|
appName: "haveno-XMR_STAGENET_bob", // bob
|
||||||
|
logProcessOutput: false,
|
||||||
|
url: "http://localhost:8081",
|
||||||
|
accountPasswordRequired: false,
|
||||||
|
accountPassword: "abctesting789",
|
||||||
|
}
|
||||||
|
],
|
||||||
maxFee: BigInt("75000000000"),
|
maxFee: BigInt("75000000000"),
|
||||||
walletSyncPeriodMs: 5000, // TODO (woodser): auto adjust higher if using remote connection
|
walletSyncPeriodMs: 5000, // TODO (woodser): auto adjust higher if using remote connection
|
||||||
daemonPollPeriodMs: 15000,
|
daemonPollPeriodMs: 15000,
|
||||||
@ -99,7 +103,8 @@ const TestConfig = {
|
|||||||
["8085", ["10004", "7780"]],
|
["8085", ["10004", "7780"]],
|
||||||
["8086", ["10005", "7781"]],
|
["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 {
|
interface TxContext {
|
||||||
@ -107,6 +112,7 @@ interface TxContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// clients
|
// clients
|
||||||
|
let startupHavenods: HavenoDaemon[] = [];
|
||||||
let arbitrator: HavenoDaemon;
|
let arbitrator: HavenoDaemon;
|
||||||
let alice: HavenoDaemon;
|
let alice: HavenoDaemon;
|
||||||
let bob: HavenoDaemon;
|
let bob: HavenoDaemon;
|
||||||
@ -118,42 +124,41 @@ let aliceWallet: any;
|
|||||||
const HAVENO_PROCESSES: HavenoDaemon[] = [];
|
const HAVENO_PROCESSES: HavenoDaemon[] = [];
|
||||||
const HAVENO_PROCESS_PORTS: string[] = [];
|
const HAVENO_PROCESS_PORTS: string[] = [];
|
||||||
|
|
||||||
|
// other config
|
||||||
|
const OFFLINE_ERR_MSG = "Http response at 400 or 500 level";
|
||||||
|
|
||||||
// -------------------------- BEFORE / AFTER TESTS ----------------------------
|
// -------------------------- BEFORE / AFTER TESTS ----------------------------
|
||||||
|
|
||||||
|
jest.setTimeout(TestConfig.timeout);
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
|
||||||
// set log level for tests
|
// 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
|
// start configured haveno daemons
|
||||||
let daemonPromises = [];
|
let promises = [];
|
||||||
daemonPromises.push(initHavenoDaemon(TestConfig.arbitrator));
|
for (let config of TestConfig.startupHavenods) promises.push(initHavenoDaemon(config));
|
||||||
daemonPromises.push(initHavenoDaemon(TestConfig.alice));
|
for (let settledPromise of await Promise.allSettled(promises)) {
|
||||||
daemonPromises.push(initHavenoDaemon(TestConfig.bob));
|
if (settledPromise.status !== "fulfilled") throw new Error((settledPromise as PromiseRejectedResult).reason);
|
||||||
let daemons = await Promise.allSettled(daemonPromises);
|
startupHavenods.push((settledPromise as PromiseFulfilledResult<HavenoDaemon>).value);
|
||||||
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;
|
// assign arbitrator alice, bob
|
||||||
else throw new Error((daemons[1] as PromiseRejectedResult).reason);
|
arbitrator = startupHavenods[0];
|
||||||
if (daemons[2].status === "fulfilled") bob = (daemons[2] as PromiseFulfilledResult<HavenoDaemon>).value;
|
alice = startupHavenods[1];
|
||||||
else throw new Error((daemons[2] as PromiseRejectedResult).reason);
|
bob = startupHavenods[2];
|
||||||
|
|
||||||
// register arbitrator as dispute agents
|
// register arbitrator as dispute agents
|
||||||
await arbitrator.registerDisputeAgent("mediator", TestConfig.devPrivilegePrivKey);
|
await arbitrator.registerDisputeAgent("mediator", TestConfig.devPrivilegePrivKey);
|
||||||
await arbitrator.registerDisputeAgent("refundagent", TestConfig.devPrivilegePrivKey);
|
await arbitrator.registerDisputeAgent("refundagent", TestConfig.devPrivilegePrivKey);
|
||||||
|
|
||||||
// connect monero clients
|
// connect monero clients
|
||||||
monerod = await monerojs.connectToDaemonRpc(TestConfig.monerod.uri, TestConfig.monerod.username, TestConfig.monerod.password);
|
monerod = await monerojs.connectToDaemonRpc(TestConfig.monerod.url, TestConfig.monerod.username, TestConfig.monerod.password);
|
||||||
aliceWallet = await monerojs.connectToWalletRpc(TestConfig.alice.walletUri, TestConfig.alice.walletUsername, TestConfig.alice.walletPassword);
|
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
|
// initialize funding wallet
|
||||||
await initFundingWallet();
|
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() => {
|
beforeEach(async() => {
|
||||||
@ -161,15 +166,11 @@ beforeEach(async() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
let stopPromises = [];
|
let promises = [];
|
||||||
if (arbitrator && arbitrator.getProcess()) stopPromises.push(stopHavenoProcess(arbitrator));
|
for (let havenod of startupHavenods) if (havenod.getProcess()) promises.push(releaseHavenoProcess(havenod));
|
||||||
if (alice && alice.getProcess()) stopPromises.push(stopHavenoProcess(alice));
|
return Promise.all(promises);
|
||||||
if (bob && bob.getProcess()) stopPromises.push(stopHavenoProcess(bob));
|
|
||||||
return Promise.all(stopPromises);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.setTimeout(500000);
|
|
||||||
|
|
||||||
// ----------------------------------- TESTS ----------------------------------
|
// ----------------------------------- TESTS ----------------------------------
|
||||||
|
|
||||||
test("Can get the version", async () => {
|
test("Can get the version", async () => {
|
||||||
@ -177,50 +178,114 @@ test("Can get the version", async () => {
|
|||||||
expect(version).toEqual(TestConfig.haveno.version);
|
expect(version).toEqual(TestConfig.haveno.version);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Can register as dispute agents", async () => {
|
test("Can manage an account", async () => {
|
||||||
await arbitrator.registerDisputeAgent("mediator", TestConfig.devPrivilegePrivKey); // TODO: bisq mediator = haveno arbitrator
|
let charlie: HavenoDaemon | undefined;
|
||||||
await arbitrator.registerDisputeAgent("refundagent", TestConfig.devPrivilegePrivKey); // TODO: no refund agent in haveno
|
let err: any;
|
||||||
|
|
||||||
// test bad dispute agent type
|
|
||||||
try {
|
try {
|
||||||
await arbitrator.registerDisputeAgent("unsupported type", TestConfig.devPrivilegePrivKey);
|
|
||||||
throw new Error("should have thrown error registering bad type");
|
// start charlie without opening account
|
||||||
} catch (err) {
|
charlie = await initHavenoDaemon({autoLogin: false});
|
||||||
if (err.message !== "unknown dispute agent type 'unsupported type'") throw new Error("Unexpected error: " + err.message);
|
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
|
async function testAccountNotOpen(havenod: HavenoDaemon): Promise<void> { // TODO: generalize this?
|
||||||
try {
|
try { await havenod.getMoneroConnections(); throw new Error("Should have thrown"); }
|
||||||
await arbitrator.registerDisputeAgent("mediator", "bad key");
|
catch (err) { assert.equal(err.message, "Account not open"); }
|
||||||
throw new Error("should have thrown error registering bad key");
|
try { await havenod.getXmrTxs(); throw new Error("Should have thrown"); }
|
||||||
} catch (err) {
|
catch (err) { assert.equal(err.message, "Account not open"); }
|
||||||
if (err.message !== "invalid registration key") throw new Error("Unexpected error: " + err.message);
|
try { await havenod.getBalances(); throw new Error("Should have thrown"); }
|
||||||
}
|
catch (err) { assert.equal(err.message, "Account not open"); }
|
||||||
});
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -231,38 +296,38 @@ test("Can manage Monero daemon connections", async () => {
|
|||||||
try {
|
try {
|
||||||
|
|
||||||
// start charlie
|
// start charlie
|
||||||
charlie = await startHavenoProcess(undefined, TestConfig.logging.logProcessOutput);
|
charlie = await initHavenoDaemon();
|
||||||
|
|
||||||
// test default connections
|
// test default connections
|
||||||
let monerodUri1 = "http://localhost:38081"; // TODO: (woodser): move to config
|
let monerodUrl1 = "http://localhost:38081"; // TODO: (woodser): move to config
|
||||||
let monerodUri2 = "http://haveno.exchange:38081";
|
let monerodUrl2 = "http://haveno.exchange:38081";
|
||||||
let connections: UriConnection[] = await charlie.getMoneroConnections();
|
let connections: UrlConnection[] = await charlie.getMoneroConnections();
|
||||||
testConnection(getConnection(connections, monerodUri1)!, monerodUri1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
|
testConnection(getConnection(connections, monerodUrl1)!, monerodUrl1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
|
||||||
testConnection(getConnection(connections, monerodUri2)!, monerodUri2, OnlineStatus.UNKNOWN, AuthenticationStatus.NO_AUTHENTICATION, 2);
|
testConnection(getConnection(connections, monerodUrl2)!, monerodUrl2, OnlineStatus.UNKNOWN, AuthenticationStatus.NO_AUTHENTICATION, 2);
|
||||||
|
|
||||||
// test default connection
|
// test default connection
|
||||||
let connection: UriConnection | undefined = await charlie.getMoneroConnection();
|
let connection: UrlConnection | undefined = await charlie.getMoneroConnection();
|
||||||
testConnection(connection!, monerodUri1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
|
assert(await charlie.isConnectedToMonero());
|
||||||
//assert(await charlie.isMoneroConnected()); // TODO (woodser): support havenod.isConnected()?
|
testConnection(connection!, monerodUrl1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
|
||||||
|
|
||||||
// add a new connection
|
// add a new connection
|
||||||
let fooBarUri = "http://foo.bar";
|
let fooBarUrl = "http://foo.bar";
|
||||||
await charlie.addMoneroConnection(fooBarUri);
|
await charlie.addMoneroConnection(fooBarUrl);
|
||||||
connections = await charlie.getMoneroConnections();
|
connections = await charlie.getMoneroConnections();
|
||||||
connection = getConnection(connections, fooBarUri);
|
connection = getConnection(connections, fooBarUrl);
|
||||||
testConnection(connection!, fooBarUri, OnlineStatus.UNKNOWN, AuthenticationStatus.NO_AUTHENTICATION, 0);
|
testConnection(connection!, fooBarUrl, OnlineStatus.UNKNOWN, AuthenticationStatus.NO_AUTHENTICATION, 0);
|
||||||
//connection = await charlie.getMoneroConnection(uri); TODO (woodser): allow getting connection by uri?
|
|
||||||
|
|
||||||
// set prioritized connection without credentials
|
// set prioritized connection without credentials
|
||||||
await charlie.setMoneroConnection(new UriConnection()
|
await charlie.setMoneroConnection(new UrlConnection()
|
||||||
.setUri(TestConfig.monerod2.uri)
|
.setUrl(TestConfig.monerod2.url)
|
||||||
.setPriority(1));
|
.setPriority(1));
|
||||||
connection = await charlie.getMoneroConnection();
|
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 is offline
|
||||||
connection = await charlie.checkMoneroConnection();
|
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
|
// start monerod2
|
||||||
let cmd = [
|
let cmd = [
|
||||||
@ -280,31 +345,37 @@ test("Can manage Monero daemon connections", async () => {
|
|||||||
|
|
||||||
// connection is online and not authenticated
|
// connection is online and not authenticated
|
||||||
connection = await charlie.checkMoneroConnection();
|
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
|
// set connection credentials
|
||||||
await charlie.setMoneroConnection(new UriConnection()
|
await charlie.setMoneroConnection(new UrlConnection()
|
||||||
.setUri(TestConfig.monerod2.uri)
|
.setUrl(TestConfig.monerod2.url)
|
||||||
.setUsername(TestConfig.monerod2.username)
|
.setUsername(TestConfig.monerod2.username)
|
||||||
.setPassword(TestConfig.monerod2.password)
|
.setPassword(TestConfig.monerod2.password)
|
||||||
.setPriority(1));
|
.setPriority(1));
|
||||||
connection = await charlie.getMoneroConnection();
|
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 is online and authenticated
|
||||||
connection = await charlie.checkMoneroConnection();
|
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
|
// restart charlie
|
||||||
let appName = charlie.getAppName();
|
let appName = charlie.getAppName();
|
||||||
await stopHavenoProcess(charlie);
|
await releaseHavenoProcess(charlie);
|
||||||
charlie = await startHavenoProcess(appName, TestConfig.logging.logProcessOutput);
|
charlie = await initHavenoDaemon({appName: appName, accountPassword: password});
|
||||||
|
|
||||||
// connection is restored, online, and authenticated
|
// connection is restored, online, and authenticated
|
||||||
connection = await charlie.getMoneroConnection();
|
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();
|
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
|
// enable auto switch
|
||||||
await charlie.setAutoSwitch(true);
|
await charlie.setAutoSwitch(true);
|
||||||
@ -315,58 +386,58 @@ test("Can manage Monero daemon connections", async () => {
|
|||||||
// test auto switch after periodic connection check
|
// test auto switch after periodic connection check
|
||||||
await wait(TestConfig.daemonPollPeriodMs);
|
await wait(TestConfig.daemonPollPeriodMs);
|
||||||
connection = await charlie.getMoneroConnection();
|
connection = await charlie.getMoneroConnection();
|
||||||
testConnection(connection!, monerodUri1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
|
testConnection(connection!, monerodUrl1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
|
||||||
|
|
||||||
// stop checking connection periodically
|
// stop checking connection periodically
|
||||||
await charlie.stopCheckingConnection();
|
await charlie.stopCheckingConnection();
|
||||||
|
|
||||||
// remove current connection
|
// remove current connection
|
||||||
await charlie.removeMoneroConnection(monerodUri1);
|
await charlie.removeMoneroConnection(monerodUrl1);
|
||||||
|
|
||||||
// check current connection
|
// check current connection
|
||||||
connection = await charlie.checkMoneroConnection();
|
connection = await charlie.checkMoneroConnection();
|
||||||
assert.equal(undefined, connection);
|
assert.equal(connection, undefined);
|
||||||
|
|
||||||
// check all connections
|
// check all connections
|
||||||
await charlie.checkMoneroConnections();
|
await charlie.checkMoneroConnections();
|
||||||
connections = await charlie.getMoneroConnections();
|
connections = await charlie.getMoneroConnections();
|
||||||
testConnection(getConnection(connections, fooBarUri)!, fooBarUri, OnlineStatus.OFFLINE, AuthenticationStatus.NO_AUTHENTICATION, 0);
|
testConnection(getConnection(connections, fooBarUrl)!, fooBarUrl, OnlineStatus.OFFLINE, AuthenticationStatus.NO_AUTHENTICATION, 0);
|
||||||
for (let connection of connections) testConnection(connection!, connection.getUri(), OnlineStatus.OFFLINE, AuthenticationStatus.NO_AUTHENTICATION);
|
for (let connection of connections) testConnection(connection!, connection.getUrl(), OnlineStatus.OFFLINE, AuthenticationStatus.NO_AUTHENTICATION);
|
||||||
|
|
||||||
// set connection to previous uri
|
// set connection to previous url
|
||||||
await charlie.setMoneroConnection(fooBarUri);
|
await charlie.setMoneroConnection(fooBarUrl);
|
||||||
connection = await charlie.getMoneroConnection();
|
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
|
// set connection to new url
|
||||||
let fooBarUri2 = "http://foo.bar.xyz";
|
let fooBarUrl2 = "http://foo.bar.xyz";
|
||||||
await charlie.setMoneroConnection(fooBarUri2);
|
await charlie.setMoneroConnection(fooBarUrl2);
|
||||||
connections = await charlie.getMoneroConnections();
|
connections = await charlie.getMoneroConnections();
|
||||||
connection = getConnection(connections, fooBarUri2);
|
connection = getConnection(connections, fooBarUrl2);
|
||||||
testConnection(connection!, fooBarUri2, OnlineStatus.UNKNOWN, AuthenticationStatus.NO_AUTHENTICATION, 0);
|
testConnection(connection!, fooBarUrl2, OnlineStatus.UNKNOWN, AuthenticationStatus.NO_AUTHENTICATION, 0);
|
||||||
|
|
||||||
// reset connection
|
// reset connection
|
||||||
await charlie.setMoneroConnection();
|
await charlie.setMoneroConnection();
|
||||||
assert.equal(undefined, await charlie.getMoneroConnection());
|
assert.equal(await charlie.getMoneroConnection(), undefined);
|
||||||
|
|
||||||
// test auto switch after start checking connection
|
// test auto switch after start checking connection
|
||||||
await charlie.setAutoSwitch(false);
|
await charlie.setAutoSwitch(false);
|
||||||
await charlie.startCheckingConnection(5000); // checks the connection
|
await charlie.startCheckingConnection(5000); // checks the connection
|
||||||
await charlie.setAutoSwitch(true);
|
await charlie.setAutoSwitch(true);
|
||||||
await charlie.addMoneroConnection(new UriConnection()
|
await charlie.addMoneroConnection(new UrlConnection()
|
||||||
.setUri(TestConfig.monerod.uri)
|
.setUrl(TestConfig.monerod.url)
|
||||||
.setUsername(TestConfig.monerod.username)
|
.setUsername(TestConfig.monerod.username)
|
||||||
.setPassword(TestConfig.monerod.password)
|
.setPassword(TestConfig.monerod.password)
|
||||||
.setPriority(2));
|
.setPriority(2));
|
||||||
await wait(10000);
|
await wait(10000);
|
||||||
connection = await charlie.getMoneroConnection();
|
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) {
|
} catch (err2) {
|
||||||
err = err2;
|
err = err2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop processes
|
// stop processes
|
||||||
if (charlie) await stopHavenoProcess(charlie);
|
if (charlie) await releaseHavenoProcess(charlie);
|
||||||
if (monerod2) await monerod2.stopProcess();
|
if (monerod2) await monerod2.stopProcess();
|
||||||
// TODO: how to delete trader app folder at end of test?
|
// TODO: how to delete trader app folder at end of test?
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
@ -432,6 +503,32 @@ test("Can get balances", async () => {
|
|||||||
expect(BigInt(balances.getReservedTradeBalance())).toBeGreaterThanOrEqual(0);
|
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 () => {
|
test("Can get market prices", async () => {
|
||||||
|
|
||||||
// get all market prices
|
// get all market prices
|
||||||
@ -465,6 +562,27 @@ test("Can get market prices", async () => {
|
|||||||
.toThrow('Currency not found: INVALID_CURRENCY');
|
.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 () => {
|
test("Can get offers", async () => {
|
||||||
let offers: OfferInfo[] = await alice.getOffers("BUY");
|
let offers: OfferInfo[] = await alice.getOffers("BUY");
|
||||||
for (let offer of offers) {
|
for (let offer of offers) {
|
||||||
@ -624,7 +742,7 @@ test("Handles unexpected errors during trade initialization", async () => {
|
|||||||
|
|
||||||
// start and fund 3 trader processes
|
// start and fund 3 trader processes
|
||||||
console.log("Starting trader processes");
|
console.log("Starting trader processes");
|
||||||
traders = await startHavenoProcesses(3, TestConfig.logging.logProcessOutput);
|
traders = await initHavenoDaemons(3);
|
||||||
console.log("Funding traders");
|
console.log("Funding traders");
|
||||||
let tradeAmount: bigint = BigInt("250000000000");
|
let tradeAmount: bigint = BigInt("250000000000");
|
||||||
await waitForUnlockedBalance(tradeAmount * BigInt("2"), traders[0], traders[1], traders[2]);
|
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]);
|
let paymentAccount = await createCryptoPaymentAccount(traders[1]);
|
||||||
wait(3000).then(async function() {
|
wait(3000).then(async function() {
|
||||||
try {
|
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());
|
for (let frozenOutput of await traderWallet.getOutputs({isFrozen: true})) await traderWallet.thawOutput(frozenOutput.getKeyImage().getHex());
|
||||||
console.log("Sweeping trade funds");
|
console.log("Sweeping trade funds");
|
||||||
await traderWallet.sweepUnlocked({address: await traderWallet.getPrimaryAddress(), relay: true});
|
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
|
// trader 0 spends trade funds then trader 2 takes offer
|
||||||
wait(3000).then(async function() {
|
wait(3000).then(async function() {
|
||||||
try {
|
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());
|
for (let frozenOutput of await traderWallet.getOutputs({isFrozen: true})) await traderWallet.thawOutput(frozenOutput.getKeyImage().getHex());
|
||||||
console.log("Sweeping offer funds");
|
console.log("Sweeping offer funds");
|
||||||
await traderWallet.sweepUnlocked({address: await traderWallet.getPrimaryAddress(), relay: true});
|
await traderWallet.sweepUnlocked({address: await traderWallet.getPrimaryAddress(), relay: true});
|
||||||
@ -706,7 +824,7 @@ test("Handles unexpected errors during trade initialization", async () => {
|
|||||||
|
|
||||||
// stop traders
|
// stop traders
|
||||||
console.log("Stopping haveno processes");
|
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;
|
if (err) throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -716,7 +834,7 @@ test("Cannot make or take offer with insufficient unlocked funds", async () => {
|
|||||||
try {
|
try {
|
||||||
|
|
||||||
// start charlie
|
// start charlie
|
||||||
charlie = await startHavenoProcess(undefined, TestConfig.logging.logProcessOutput);
|
charlie = await initHavenoDaemon();
|
||||||
|
|
||||||
// charlie creates ethereum payment account
|
// charlie creates ethereum payment account
|
||||||
let paymentAccount = await createCryptoPaymentAccount(charlie);
|
let paymentAccount = await createCryptoPaymentAccount(charlie);
|
||||||
@ -728,7 +846,7 @@ test("Cannot make or take offer with insufficient unlocked funds", async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
let errTyped = err as grpcWeb.RpcError;
|
let errTyped = err as grpcWeb.RpcError;
|
||||||
assert.equal(errTyped.code, 2);
|
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
|
// 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")
|
throw new Error("Should have failed taking offer with insufficient funds")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let errTyped = err as grpcWeb.RpcError;
|
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);
|
assert.equal(errTyped.code, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -766,7 +884,7 @@ test("Cannot make or take offer with insufficient unlocked funds", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// stop charlie
|
// stop charlie
|
||||||
if (charlie) await stopHavenoProcess(charlie);
|
if (charlie) await releaseHavenoProcess(charlie);
|
||||||
// TODO: how to delete trader app folder at end of test?
|
// TODO: how to delete trader app folder at end of test?
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
});
|
});
|
||||||
@ -899,117 +1017,95 @@ test("Can complete a trade", async () => {
|
|||||||
|
|
||||||
// ------------------------------- HELPERS ------------------------------------
|
// ------------------------------- HELPERS ------------------------------------
|
||||||
|
|
||||||
function getConnection(connections: UriConnection[], uri: string): UriConnection | undefined {
|
async function initHavenoDaemons(numDaemons: number, config?: any) {
|
||||||
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[]> {
|
|
||||||
let traderPromises: Promise<HavenoDaemon>[] = [];
|
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);
|
return Promise.all(traderPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async function initHavenoDaemon(config?: any): Promise<HavenoDaemon> {
|
||||||
* Start a Haveno daemon as a process.
|
config = Object.assign({}, TestConfig.defaultHavenod, config);
|
||||||
*
|
if (!config.appName) config.appName = "haveno-XMR_STAGENET_instance_" + GenUtils.getUUID();
|
||||||
* 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();
|
|
||||||
|
|
||||||
// get proxy port for haveno process
|
// connect to existing server or start new process
|
||||||
let proxyPort;
|
let havenod;
|
||||||
if (appName === TestConfig.arbitrator.appName) proxyPort = "8079";
|
try {
|
||||||
else if (appName === TestConfig.alice.appName) proxyPort = "8080";
|
|
||||||
else if (appName === TestConfig.bob.appName) proxyPort = "8081";
|
// try to connect to existing server
|
||||||
else {
|
havenod = new HavenoDaemon(config.url, config.apiPassword);
|
||||||
for (let port of Array.from(TestConfig.proxyPorts.keys())) {
|
await havenod.getVersion();
|
||||||
if (port === "8079" || port === "8080" || port === "8081") continue; // reserved for arbitrator, alice, and bob
|
} catch (err) {
|
||||||
if (!GenUtils.arrayContains(HAVENO_PROCESS_PORTS, port)) {
|
|
||||||
HAVENO_PROCESS_PORTS.push(port);
|
// get port for haveno process
|
||||||
proxyPort = port;
|
let proxyPort = "";
|
||||||
break;
|
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
|
// open account if configured
|
||||||
let cmd: string[] = [
|
if (config.autoLogin) await initHavenoAccount(havenod, config.accountPassword);
|
||||||
"./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);
|
|
||||||
return havenod;
|
return havenod;
|
||||||
}
|
|
||||||
|
async function getAvailablePort(): Promise<number> {
|
||||||
/**
|
return new Promise(function(resolve, reject) {
|
||||||
* Get a free port.
|
let srv = net.createServer();
|
||||||
*/
|
srv.listen(0, function() {
|
||||||
async function getFreePort(): Promise<number> {
|
let port = srv.address().port;
|
||||||
return new Promise(function(resolve, reject) {
|
srv.close(function() {
|
||||||
let srv = net.createServer();
|
resolve(port);
|
||||||
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) {
|
async function releaseHavenoProcess(havenod: HavenoDaemon) {
|
||||||
await havenod.stopProcess();
|
|
||||||
GenUtils.remove(HAVENO_PROCESSES, havenod);
|
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() {
|
async function initFundingWallet() {
|
||||||
|
|
||||||
// init client connected to monero-wallet-rpc
|
// 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
|
// check if wallet is open
|
||||||
let walletIsOpen = false
|
let walletIsOpen = false
|
||||||
@ -1164,6 +1260,20 @@ function getNotifications(notifications: NotificationMessage[], notificationType
|
|||||||
return filteredNotifications;
|
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) {
|
function testTx(tx: XmrTx, ctx: TxContext) {
|
||||||
assert(tx.getHash());
|
assert(tx.getHash());
|
||||||
expect(BigInt(tx.getFee())).toBeLessThan(TestConfig.maxFee);
|
expect(BigInt(tx.getFee())).toBeLessThan(TestConfig.maxFee);
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import {HavenoUtils} from "./utils/HavenoUtils";
|
import {HavenoUtils} from "./utils/HavenoUtils";
|
||||||
import {TaskLooper} from "./utils/TaskLooper";
|
import {TaskLooper} from "./utils/TaskLooper";
|
||||||
import * as grpcWeb from 'grpc-web';
|
import * as grpcWeb from 'grpc-web';
|
||||||
import {DisputeAgentsClient, GetVersionClient, NotificationsClient, PriceClient, WalletsClient, OffersClient, PaymentAccountsClient, TradesClient, MoneroConnectionsClient} from './protobuf/GrpcServiceClientPb';
|
import {GetVersionClient, AccountClient, MoneroConnectionsClient, DisputeAgentsClient, NotificationsClient, WalletsClient, PriceClient, OffersClient, PaymentAccountsClient, TradesClient, ShutdownServerClient} 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 {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 {AvailabilityResult, PaymentAccount} from './protobuf/pb_pb';
|
import {PaymentAccount, AvailabilityResult} from './protobuf/pb_pb';
|
||||||
const console = require('console');
|
const console = require('console');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -12,6 +12,7 @@ const console = require('console');
|
|||||||
class HavenoDaemon {
|
class HavenoDaemon {
|
||||||
|
|
||||||
// grpc clients
|
// grpc clients
|
||||||
|
_appName: string|undefined;
|
||||||
_getVersionClient: GetVersionClient;
|
_getVersionClient: GetVersionClient;
|
||||||
_disputeAgentsClient: DisputeAgentsClient;
|
_disputeAgentsClient: DisputeAgentsClient;
|
||||||
_notificationsClient: NotificationsClient;
|
_notificationsClient: NotificationsClient;
|
||||||
@ -21,17 +22,24 @@ class HavenoDaemon {
|
|||||||
_paymentAccountsClient: PaymentAccountsClient;
|
_paymentAccountsClient: PaymentAccountsClient;
|
||||||
_offersClient: OffersClient;
|
_offersClient: OffersClient;
|
||||||
_tradesClient: TradesClient;
|
_tradesClient: TradesClient;
|
||||||
|
_accountClient: AccountClient;
|
||||||
|
_shutdownServerClient: ShutdownServerClient;
|
||||||
|
|
||||||
// other instance variables
|
// state variables
|
||||||
_url: string;
|
_url: string;
|
||||||
_password: string;
|
_password: string;
|
||||||
_process: any;
|
_process: any;
|
||||||
_processLogging: boolean = false;
|
_processLogging = false;
|
||||||
_walletRpcPort: number|undefined;
|
_walletRpcPort: number|undefined;
|
||||||
_notificationListeners: ((notification: NotificationMessage) => void)[] = [];
|
_notificationListeners: ((notification: NotificationMessage) => void)[] = [];
|
||||||
|
_registerNotificationListenerCalled = false;
|
||||||
|
_keepAliveLooper: any;
|
||||||
_keepAlivePeriodMs: number = 60000;
|
_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.
|
* Construct a client connected to a Haveno daemon.
|
||||||
*
|
*
|
||||||
@ -39,20 +47,22 @@ class HavenoDaemon {
|
|||||||
* @param {string} password - Haveno daemon password
|
* @param {string} password - Haveno daemon password
|
||||||
*/
|
*/
|
||||||
constructor(url: string, password: string) {
|
constructor(url: string, password: string) {
|
||||||
HavenoUtils.log(2, "Creating HavenoDaemon(" + url + ", " + password + ")");
|
|
||||||
if (!url) throw new Error("Must provide URL of Haveno daemon");
|
if (!url) throw new Error("Must provide URL of Haveno daemon");
|
||||||
if (!password) throw new Error("Must provide password 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._url = url;
|
||||||
this._password = password;
|
this._password = password;
|
||||||
this._getVersionClient = new GetVersionClient(this._url);
|
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._moneroConnectionsClient = new MoneroConnectionsClient(this._url)
|
||||||
|
this._disputeAgentsClient = new DisputeAgentsClient(this._url);
|
||||||
this._walletsClient = new WalletsClient(this._url);
|
this._walletsClient = new WalletsClient(this._url);
|
||||||
this._priceClient = new PriceClient(this._url);
|
this._priceClient = new PriceClient(this._url);
|
||||||
this._paymentAccountsClient = new PaymentAccountsClient(this._url);
|
this._paymentAccountsClient = new PaymentAccountsClient(this._url);
|
||||||
this._offersClient = new OffersClient(this._url);
|
this._offersClient = new OffersClient(this._url);
|
||||||
this._tradesClient = new TradesClient(this._url);
|
this._tradesClient = new TradesClient(this._url);
|
||||||
this._notificationsClient = new NotificationsClient(this._url);
|
this._notificationsClient = new NotificationsClient(this._url);
|
||||||
|
this._shutdownServerClient = new ShutdownServerClient(this._url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,7 +82,7 @@ class HavenoDaemon {
|
|||||||
|
|
||||||
// state variables
|
// state variables
|
||||||
let output = "";
|
let output = "";
|
||||||
let isResolved = false;
|
let isStarted = false;
|
||||||
let daemon: HavenoDaemon|undefined = undefined;
|
let daemon: HavenoDaemon|undefined = undefined;
|
||||||
|
|
||||||
// start process
|
// start process
|
||||||
@ -83,11 +93,11 @@ class HavenoDaemon {
|
|||||||
// handle stdout
|
// handle stdout
|
||||||
childProcess.stdout.on('data', async function(data: any) {
|
childProcess.stdout.on('data', async function(data: any) {
|
||||||
let line = data.toString();
|
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
|
output += line + '\n'; // capture output in case of error
|
||||||
|
|
||||||
// read success message
|
// initialize daemon on success or login required message
|
||||||
if (line.indexOf("HavenoHeadlessAppMain: onSetupComplete") >= 0) {
|
if (!daemon && (line.indexOf(HavenoDaemon._fullyInitializedMessage) >= 0 || line.indexOf(HavenoDaemon._loginRequiredMessage) >= 0)) {
|
||||||
|
|
||||||
// get api password
|
// get api password
|
||||||
let passwordIdx = cmd.indexOf("--apiPassword");
|
let passwordIdx = cmd.indexOf("--apiPassword");
|
||||||
@ -108,40 +118,40 @@ class HavenoDaemon {
|
|||||||
if (walletRpcPortIdx >= 0) daemon._walletRpcPort = parseInt(cmd[walletRpcPortIdx + 1]);
|
if (walletRpcPortIdx >= 0) daemon._walletRpcPort = parseInt(cmd[walletRpcPortIdx + 1]);
|
||||||
|
|
||||||
// resolve promise with client connected to internal process
|
// resolve promise with client connected to internal process
|
||||||
isResolved = true;
|
isStarted = true;
|
||||||
resolve(daemon);
|
resolve(daemon);
|
||||||
}
|
}
|
||||||
|
|
||||||
// read error message
|
// read error message
|
||||||
if (line.indexOf("[HavenoDaemonMain] ERROR") >= 0) {
|
if (line.indexOf("[HavenoDaemonMain] ERROR") >= 0) {
|
||||||
if (!isResolved) await rejectProcess(new Error(line));
|
if (!isStarted) await rejectStartup(new Error(line));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// handle stderr
|
// handle stderr
|
||||||
childProcess.stderr.on('data', function(data: any) {
|
childProcess.stderr.on('data', function(data: any) {
|
||||||
if (HavenoUtils.getLogLevel() >= 2 && loggingEnabled()) process.stderr.write(data);
|
if (loggingEnabled()) process.stderr.write(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// handle exit
|
// handle exit
|
||||||
childProcess.on("exit", async function(code: any) {
|
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
|
// handle error
|
||||||
childProcess.on("error", async function(err: any) {
|
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 (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
|
// handle uncaught exception
|
||||||
childProcess.on("uncaughtException", async function(err: any, origin: any) {
|
childProcess.on("uncaughtException", async function(err: any, origin: any) {
|
||||||
console.error("Uncaught exception in Haveno process: " + err.message);
|
console.error("Uncaught exception in Haveno process: " + err.message);
|
||||||
console.error(origin);
|
console.error(origin);
|
||||||
await rejectProcess(err);
|
await rejectStartup(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function rejectProcess(err: any) {
|
async function rejectStartup(err: any) {
|
||||||
await HavenoUtils.kill(childProcess);
|
await HavenoUtils.kill(childProcess);
|
||||||
reject(err);
|
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.
|
* 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
|
* @return {boolean} true if connected with the Haveno daemon, false otherwise
|
||||||
* @param {string} registrationKey - registration key
|
|
||||||
*/
|
*/
|
||||||
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 that = this;
|
||||||
let request = new RegisterDisputeAgentRequest()
|
|
||||||
.setDisputeAgentType(disputeAgentType)
|
|
||||||
.setRegistrationKey(registrationKey);
|
|
||||||
return new Promise(function(resolve, reject) {
|
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);
|
if (err) reject(err);
|
||||||
else resolve();
|
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
|
* @param {HavenoDaemonListener} listener - the notification listener to add
|
||||||
*/
|
*/
|
||||||
async addNotificationListener(listener: (notification: NotificationMessage) => void): Promise<void> {
|
async addNotificationListener(listener: (notification: NotificationMessage) => void): Promise<void> {
|
||||||
this._notificationListeners.push(listener);
|
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.
|
* 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;
|
let that = this;
|
||||||
return new Promise(function(resolve, reject) {
|
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);
|
if (err) reject(err);
|
||||||
else resolve();
|
else resolve();
|
||||||
});
|
});
|
||||||
@ -266,12 +436,12 @@ class HavenoDaemon {
|
|||||||
/**
|
/**
|
||||||
* Remove a Monero daemon connection.
|
* 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;
|
let that = this;
|
||||||
return new Promise(function(resolve, reject) {
|
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);
|
if (err) reject(err);
|
||||||
else resolve();
|
else resolve();
|
||||||
});
|
});
|
||||||
@ -281,9 +451,9 @@ class HavenoDaemon {
|
|||||||
/**
|
/**
|
||||||
* Get the current Monero daemon connection.
|
* 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;
|
let that = this;
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
that._moneroConnectionsClient.getConnection(new GetConnectionRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: GetConnectionReply) {
|
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.
|
* 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;
|
let that = this;
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
that._moneroConnectionsClient.getConnections(new GetConnectionsRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: GetConnectionsReply) {
|
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.
|
* Set the current Monero daemon connection.
|
||||||
*
|
*
|
||||||
* Add the connection if not previously seen.
|
* 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 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.
|
* 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 that = this;
|
||||||
let request = new SetConnectionRequest();
|
let request = new SetConnectionRequest();
|
||||||
if (typeof connection === "string") request.setUri(connection);
|
if (typeof connection === "string") request.setUrl(connection);
|
||||||
else request.setConnection(connection);
|
else request.setConnection(connection);
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
that._moneroConnectionsClient.setConnection(request, {password: that._password}, function(err: grpcWeb.RpcError) {
|
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.
|
* 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;
|
let that = this;
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
that._moneroConnectionsClient.checkConnection(new CheckConnectionRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: CheckConnectionReply) {
|
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.
|
* 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;
|
let that = this;
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
that._moneroConnectionsClient.checkConnections(new CheckConnectionsRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: CheckConnectionsReply) {
|
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.
|
* 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;
|
let that = this;
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
that._moneroConnectionsClient.getBestAvailableConnection(new GetBestAvailableConnectionRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: GetBestAvailableConnectionReply) {
|
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.
|
* 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 ----------------------------------
|
// ------------------------------- 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.
|
* Register a listener to receive notifications.
|
||||||
* Due to the nature of grpc streaming, this method returns a promise
|
* Due to the nature of grpc streaming, this method returns a promise
|
||||||
* which may be resolved before the listener is actually registered.
|
* 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;
|
let that = this;
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
|
|
||||||
// send request to register client listener
|
// send request to register client listener
|
||||||
that._notificationsClient.registerNotificationListener(new RegisterNotificationListenerRequest(), {password: that._password})
|
that._notificationsClient.registerNotificationListener(new RegisterNotificationListenerRequest(), {password: that._password})
|
||||||
.on("data", (data) => {
|
.on('data', (data) => {
|
||||||
if (data instanceof NotificationMessage) {
|
if (data instanceof NotificationMessage) {
|
||||||
for (let listener of that._notificationListeners) listener(data);
|
for (let listener of that._notificationListeners) listener(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// periodically send keep alive requests // TODO (woodser): better way to keep notification stream alive?
|
// periodically send keep alive requests // TODO (woodser): better way to keep notification stream alive?
|
||||||
let firstRequest = true;
|
let firstRequest = true;
|
||||||
let taskLooper = new TaskLooper(async function() {
|
that._keepAliveLooper = new TaskLooper(async function() {
|
||||||
if (firstRequest) {
|
if (firstRequest) {
|
||||||
firstRequest = false;
|
firstRequest = false;
|
||||||
return;
|
return;
|
||||||
@ -800,16 +1048,15 @@ class HavenoDaemon {
|
|||||||
.setType(NotificationMessage.NotificationType.KEEP_ALIVE)
|
.setType(NotificationMessage.NotificationType.KEEP_ALIVE)
|
||||||
.setTimestamp(Date.now()));
|
.setTimestamp(Date.now()));
|
||||||
});
|
});
|
||||||
taskLooper.start(that._keepAlivePeriodMs);
|
that._keepAliveLooper.start(that._keepAlivePeriodMs);
|
||||||
|
|
||||||
// TODO: call returns before listener registered
|
setTimeout(resolve, 1000); // TODO: call returns before listener registered
|
||||||
setTimeout(function() { resolve(); }, 1000);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a notification.
|
* Send a notification.
|
||||||
*
|
*
|
||||||
* @param {NotificationMessage} notification - notification to send
|
* @param {NotificationMessage} notification - notification to send
|
||||||
*/
|
*/
|
||||||
async _sendNotification(notification: NotificationMessage): Promise<void> {
|
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};
|
export {HavenoDaemon};
|
||||||
|
@ -43,13 +43,14 @@ class HavenoUtils {
|
|||||||
*
|
*
|
||||||
* TODO (woodser): move this to monero-javascript GenUtils.js as common utility
|
* 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) {
|
return new Promise(function(resolve, reject) {
|
||||||
process.on("exit", function() { resolve(); });
|
process.on("exit", function() { resolve(); });
|
||||||
process.on("error", function(err: any) { reject(err); });
|
process.on("error", function(err: any) { reject(err); });
|
||||||
process.kill("SIGINT");
|
process.kill(signal ? signal : "SIGINT");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user