add ability to start haveno processes from tests

test making or taking offer with insufficient unlocked balance
This commit is contained in:
woodser 2021-12-08 06:22:36 -05:00
parent 5dc5fe45d7
commit fe2c715d80
5 changed files with 562 additions and 47 deletions

View File

@ -30,7 +30,7 @@ Running the [top-level API tests](./src/HavenoDaemon.test.ts) is a great way to
1. [Run a local Haveno test network](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md), running Alice and Bob as daemons with `make alice-daemon` and `make bob-daemon`.
2. Clone this project to the same parent directory as the haveno project: `git clone https://github.com/haveno-dex/haveno-ui-poc`
3. In a new terminal, start envoy with the config in haveno-ui-poc/config/envoy.test.yaml (change absolute path for your system): `docker run --rm --add-host host.docker.internal:host-gateway -it -v ~/git/haveno-ui-poc/config/envoy.test.yaml:/envoy.test.yaml -p 8080:8080 -p 8081:8081 envoyproxy/envoy-dev:8a2143613d43d17d1eb35a24b4a4a4c432215606 -c /envoy.test.yaml`
3. In a new terminal, start envoy with the config in haveno-ui-poc/config/envoy.test.yaml (change absolute path for your system): `docker run --rm --add-host host.docker.internal:host-gateway -it -v ~/git/haveno-ui-poc/config/envoy.test.yaml:/envoy.test.yaml -p 8080:8080 -p 8081:8081 -p 8082:8082 -p 8083:8083 -p 8084:8084 -p 8085:8085 -p 8086:8086 envoyproxy/envoy-dev:8a2143613d43d17d1eb35a24b4a4a4c432215606 -c /envoy.test.yaml`
4. In a new terminal, start the funding wallet. This wallet will be automatically funded in order to fund Alice and Bob during the tests.<br>For example: `cd ~/git/haveno && make funding-wallet`.
5. Install protobuf for your system:<br>
mac: `brew install protobuf`<br>

View File

@ -1,4 +1,4 @@
# envoy configuration to test with alice and bob trader instances
# envoy configuration to test with haveno instances
admin:
access_log_path: /tmp/admin_access.log
@ -73,6 +73,171 @@ static_resources:
- name: envoy.filters.http.grpc_web
- name: envoy.filters.http.cors
- name: envoy.filters.http.router
- name: haveno1_listener
address:
socket_address: { address: 0.0.0.0, port_value: 8082 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: haveno1_service
timeout: 0s
max_stream_duration:
grpc_timeout_header_max: 0s
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: password,keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.filters.http.grpc_web
- name: envoy.filters.http.cors
- name: envoy.filters.http.router
- name: haveno2_listener
address:
socket_address: { address: 0.0.0.0, port_value: 8083 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: haveno2_service
timeout: 0s
max_stream_duration:
grpc_timeout_header_max: 0s
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: password,keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.filters.http.grpc_web
- name: envoy.filters.http.cors
- name: envoy.filters.http.router
- name: haveno3_listener
address:
socket_address: { address: 0.0.0.0, port_value: 8084 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: haveno3_service
timeout: 0s
max_stream_duration:
grpc_timeout_header_max: 0s
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: password,keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.filters.http.grpc_web
- name: envoy.filters.http.cors
- name: envoy.filters.http.router
- name: haveno4_listener
address:
socket_address: { address: 0.0.0.0, port_value: 8085 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: haveno4_service
timeout: 0s
max_stream_duration:
grpc_timeout_header_max: 0s
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: password,keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.filters.http.grpc_web
- name: envoy.filters.http.cors
- name: envoy.filters.http.router
- name: haveno5_listener
address:
socket_address: { address: 0.0.0.0, port_value: 8086 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: haveno5_service
timeout: 0s
max_stream_duration:
grpc_timeout_header_max: 0s
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: password,keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.filters.http.grpc_web
- name: envoy.filters.http.cors
- name: envoy.filters.http.router
clusters:
- name: alice_service
connect_timeout: 0.25s
@ -102,3 +267,73 @@ static_resources:
socket_address:
address: host.docker.internal
port_value: 10000
- name: haveno1_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
load_assignment:
cluster_name: cluster_0
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: host.docker.internal
port_value: 10001
- name: haveno2_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
load_assignment:
cluster_name: cluster_0
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: host.docker.internal
port_value: 10002
- name: haveno3_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
load_assignment:
cluster_name: cluster_0
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: host.docker.internal
port_value: 10003
- name: haveno4_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
load_assignment:
cluster_name: cluster_0
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: host.docker.internal
port_value: 10004
- name: haveno5_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
load_assignment:
cluster_name: cluster_0
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: host.docker.internal
port_value: 10005

View File

@ -2,6 +2,8 @@
// import haveno types
import {HavenoDaemon} from "./HavenoDaemon";
import {HavenoUtils} from "./HavenoUtils";
import * as grpcWeb from 'grpc-web';
import {XmrBalanceInfo, OfferInfo, TradeInfo, MarketPriceInfo} from './protobuf/grpc_pb'; // TODO (woodser): better names; haveno_grpc_pb, haveno_pb
import {PaymentAccount, Offer} from './protobuf/pb_pb';
@ -11,38 +13,45 @@ const GenUtils = monerojs.GenUtils;
const MoneroTxConfig = monerojs.MoneroTxConfig;
const TaskLooper = monerojs.TaskLooper;
// import console because jest swallows messages in real time
const console = require('console');
// other required imports
const console = require('console'); // import console because jest swallows messages in real time
const assert = require("assert");
// --------------------------- TEST CONFIGURATION -----------------------------
// set log level (gets more verbose increasing from 0)
HavenoUtils.setLogLevel(0);
// path to directory with haveno binaries
const HAVENO_PATH = "../haveno";
// wallet to fund alice and bob during tests
const fundingWalletUrl = "http://localhost:38084";
const fundingWalletUsername = "rpc_user";
const fundingWalletPassword = "abc123";
const defaultFundingWalletPath = "test_funding_wallet";
const minimumFunding = BigInt("5000000000000");
const FUNDING_WALLET_URL = "http://localhost:38084";
const FUNDING_WALLET_USERNAME = "rpc_user";
const FUNDING_WALLET_PASSWORD = "abc123";
const DEFAULT_FUNDING_WALLET_PATH = "test_funding_wallet";
const MINIMUM_FUNDING = BigInt("5000000000000");
let fundingWallet: any;
// alice config
const havenoVersion = "1.6.2";
const aliceDaemonUrl = "http://localhost:8080";
const aliceDaemonPassword = "apitest";
const alice: HavenoDaemon = new HavenoDaemon(aliceDaemonUrl, aliceDaemonPassword);
const aliceWalletUrl = "http://127.0.0.1:38091"; // alice's internal haveno wallet for direct testing
const aliceWalletUsername = "rpc_user";
const aliceWalletPassword = "abc123";
const HAVENO_VERSION = "1.6.2";
const ALICE_DAEMON_URL = "http://localhost:8080";
const ALICE_DAEMON_PASSWORD = "apitest";
const ALICE_WALLET_URL = "http://127.0.0.1:38091"; // alice's internal haveno wallet for direct testing
const ALICE_WALLET_USERNAME = "rpc_user";
const ALICE_WALLET_PASSWORD = "abc123";
let alice: HavenoDaemon;
let aliceWallet: any;
// bob config
const bobDaemonUrl = "http://localhost:8081";
const bobDaemonPassword = "apitest";
const bob: HavenoDaemon = new HavenoDaemon(bobDaemonUrl, bobDaemonPassword);
const BOB_DAEMON_URL = "http://localhost:8081";
const BOB_DAEMON_PASSWORD = "apitest";
let bob: HavenoDaemon = new HavenoDaemon(BOB_DAEMON_URL, BOB_DAEMON_PASSWORD);
// monero daemon config
const moneroDaemonUrl = "http://localhost:38081"
const moneroDaemonUsername = "superuser";
const moneroDaemonPassword = "abctesting123";
const MONERO_DAEMON_URL = "http://localhost:38081"
const MONERO_DAEMON_USERNAME = "superuser";
const MONERO_DAEMON_PASSWORD = "abctesting123";
let monerod: any;
// other test config
@ -60,15 +69,29 @@ const TEST_CRYPTO_ACCOUNTS = [ // TODO (woodser): test other cryptos, fiat
}
];
// map proxied ports to havenod api and p2p ports
const PROXY_PORTS = new Map<string, string[]>([
["8080", ["9999", "5555"]],
["8081", ["10000", "6666"]],
["8082", ["10001", "7777"]],
["8083", ["10002", "7778"]],
["8084", ["10003", "7779"]],
["8085", ["10004", "7780"]],
["8086", ["10005", "7781"]],
]);
// track started haveno processes
const HAVENO_PROCESSES: HavenoDaemon[] = [];
// ----------------------------------- TESTS ----------------------------------
beforeAll(async () => {
// initialize client of monerod
monerod = await monerojs.connectToDaemonRpc(moneroDaemonUrl, moneroDaemonUsername, moneroDaemonPassword);
// create client connected to alice's internal wallet
aliceWallet = await monerojs.connectToWalletRpc(aliceWalletUrl, aliceWalletUsername, aliceWalletPassword);
// initialize clients
alice = new HavenoDaemon(ALICE_DAEMON_URL, ALICE_DAEMON_PASSWORD);
bob = new HavenoDaemon(BOB_DAEMON_URL, BOB_DAEMON_PASSWORD);
monerod = await monerojs.connectToDaemonRpc(MONERO_DAEMON_URL, MONERO_DAEMON_USERNAME, MONERO_DAEMON_PASSWORD);
aliceWallet = await monerojs.connectToWalletRpc(ALICE_WALLET_URL, ALICE_WALLET_USERNAME, ALICE_WALLET_PASSWORD);
// initialize funding wallet
await initFundingWallet();
@ -83,7 +106,7 @@ beforeAll(async () => {
jest.setTimeout(300000);
test("Can get the version", async () => {
let version = await alice.getVersion();
expect(version).toEqual(havenoVersion);
expect(version).toEqual(HAVENO_VERSION);
});
test("Can get market prices", async () => {
@ -201,7 +224,7 @@ test("Can post and remove an offer", async () => {
let unlockedBalanceBefore: bigint = BigInt((await alice.getBalances()).getUnlockedBalance());
// post offer
let offer: OfferInfo = await postOffer(alice, "buy", BigInt("200000000000"));
let offer: OfferInfo = await postOffer(alice, "buy", BigInt("200000000000"), undefined);
// cancel offer
await alice.removeOffer(offer.getId());
@ -225,7 +248,7 @@ test("Invalidates offers when reserved funds are spent", async () => {
// post offer
await wait(1000);
let offer: OfferInfo = await postOffer(alice, "buy", tradeAmount);
let offer: OfferInfo = await postOffer(alice, "buy", tradeAmount, undefined);
// get key images reserved by offer
let reservedKeyImages = [];
@ -266,6 +289,70 @@ test("Invalidates offers when reserved funds are spent", async () => {
await monerod.flushTxPool(tx.getHash());
});
test("Cannot make or take offer with insufficient unlocked funds", async () => {
let charlie: HavenoDaemon | undefined;
let err: any;
try {
// start charlie
charlie = await startTraderProcess();
// charlie creates ethereum payment account
let testAccount = TEST_CRYPTO_ACCOUNTS[0];
let ethPaymentAccount: PaymentAccount = await charlie.createCryptoPaymentAccount(
testAccount.currencyCode + " " + testAccount.address.substr(0, 8) + "... " + GenUtils.getUUID(),
testAccount.currencyCode,
testAccount.address);
// charlie cannot make offer with insufficient funds
try {
await postOffer(charlie, "buy", BigInt("200000000000"), ethPaymentAccount.getId());
throw new Error("Should have failed making offer with insufficient funds")
} catch (err) {
let errTyped = err as grpcWeb.RpcError;
assert.equal(errTyped.code, 2);
assert(errTyped.message.includes("not enough money"));
}
// alice posts offer
let offers: OfferInfo[] = await alice.getMyOffers("buy"); // TODO: support alice.getMyOffers() without direction
let offer: OfferInfo;
if (offers.length) offer = offers[0];
else {
let tradeAmount: bigint = BigInt("250000000000");
await waitForUnlockedBalance(tradeAmount * BigInt("2"), alice);
offer = await postOffer(alice, "buy", tradeAmount, undefined);
assert.equal(offer.getState(), "AVAILABLE");
}
// charlie cannot take offer with insufficient funds
try {
await charlie.takeOffer(offer.getId(), ethPaymentAccount.getId()); // TODO (woodser): this returns before trade is fully initialized. this fails with bad error message if trade is not yet seen by peer
throw new Error("Should have failed taking offer with insufficient funds")
} catch (err) {
let errTyped = err as grpcWeb.RpcError;
assert.equal(errTyped.code, 2);
assert(errTyped.message.includes("not enough money")); // TODO (woodser): error message does not contain stacktrace
}
// charlie does not have trade
try {
await charlie.getTrade(offer.getId());
} catch (err) {
let errTyped = err as grpcWeb.RpcError;
assert.equal(errTyped.code, 3);
assert(errTyped.message.includes("trade with id '" + offer.getId() + "' not found")); // TODO (woodser): error message does not contain stacktrace
}
} catch (err2) {
err = err2;
}
// stop charlie
if (charlie) await stopHavenoProcess(charlie);
// TODO: how to delete trader app folder at end of test?
if (err) throw err;
});
// TODO (woodser): test grpc notifications
test("Can complete a trade", async () => {
@ -278,7 +365,7 @@ test("Can complete a trade", async () => {
// alice posts offer to buy xmr
console.log("Alice posting offer");
let direction = "buy";
let offer: OfferInfo = await postOffer(alice, direction, tradeAmount);
let offer: OfferInfo = await postOffer(alice, direction, tradeAmount, undefined);
expect(offer.getState()).toEqual("AVAILABLE");
console.log("Alice done posting offer");
@ -368,13 +455,60 @@ test("Can complete a trade", async () => {
// ------------------------------- HELPERS ------------------------------------
/**
* Start a Haveno trader process.
*
* @return {HavenoDaemon} the client connected to the started Haveno process
*/
async function startTraderProcess(): Promise<HavenoDaemon> {
// iterate to find unused proxy port
for (let port of Array.from(PROXY_PORTS.keys())) {
if (port === "8080" || port === "8081") continue; // reserved for alice and bob
let used = false;
for (let havenod of HAVENO_PROCESSES) {
if (port === new URL(havenod.getUrl()).port) {
used = true;
break;
}
}
// start haveno process on unused port
if (!used) {
let appName = "haveno-XMR_STAGENET_trader_" + GenUtils.getUUID();
let cmd: string[] = [
"./haveno-daemon",
"--baseCurrencyNetwork", "XMR_STAGENET",
"--useLocalhostForP2P", "true",
"--useDevPrivilegeKeys", "true",
"--nodePort", PROXY_PORTS.get(port)![1],
"--appName", appName,
"--apiPassword", "apitest",
"--apiPort", PROXY_PORTS.get(port)![0]
];
let havenod = await HavenoDaemon.startProcess(HAVENO_PATH, cmd, "http://localhost:" + port);
HAVENO_PROCESSES.push(havenod);
return havenod;
}
}
throw new Error("No unused test ports available");
}
/**
* Stop a Haveno trader process and release its ports for reuse.
*/
async function stopHavenoProcess(havenod: HavenoDaemon) {
await havenod.stopProcess();
GenUtils.remove(HAVENO_PROCESSES, havenod);
}
/**
* Open or create funding wallet.
*/
async function initFundingWallet() {
// init client connected to monero-wallet-rpc
fundingWallet = await monerojs.connectToWalletRpc(fundingWalletUrl, fundingWalletUsername, fundingWalletPassword);
fundingWallet = await monerojs.connectToWalletRpc(FUNDING_WALLET_URL, FUNDING_WALLET_USERNAME, FUNDING_WALLET_PASSWORD);
// check if wallet is open
let walletIsOpen = false
@ -388,7 +522,7 @@ async function initFundingWallet() {
// attempt to open funding wallet
try {
await fundingWallet.openWallet({path: defaultFundingWalletPath, password: fundingWalletPassword});
await fundingWallet.openWallet({path: DEFAULT_FUNDING_WALLET_PATH, password: FUNDING_WALLET_PASSWORD});
} catch (e) {
if (!(e instanceof monerojs.MoneroRpcError)) throw e;
@ -396,7 +530,7 @@ async function initFundingWallet() {
if (e.getCode() === -1) {
// create wallet
await fundingWallet.createWallet({path: defaultFundingWalletPath, password: fundingWalletPassword});
await fundingWallet.createWallet({path: DEFAULT_FUNDING_WALLET_PATH, password: FUNDING_WALLET_PASSWORD});
} else {
throw e;
}
@ -447,7 +581,7 @@ async function waitForUnlockedBalance(amount: bigint, ...wallets: any[]) {
if (depositNeeded > BigInt("0") && wallet._wallet !== fundingWallet) fundConfig.addDestination(await wallet.getDepositAddress(), depositNeeded * BigInt("10")); // deposit 10 times more than needed
}
if (fundConfig.getDestinations()) {
await waitForUnlockedBalance(minimumFunding, fundingWallet); // TODO (woodser): wait for enough to cover tx amount + fee
await waitForUnlockedBalance(MINIMUM_FUNDING, fundingWallet); // TODO (woodser): wait for enough to cover tx amount + fee
try { await fundingWallet.createTx(fundConfig); }
catch (err) { throw new Error("Error funding wallets: " + err.message); }
}
@ -506,14 +640,17 @@ async function wait(durationMs: number) {
return new Promise(function(resolve) { setTimeout(resolve, durationMs); });
}
async function postOffer(maker: HavenoDaemon, direction: string, amount: bigint) {
async function postOffer(maker: HavenoDaemon, direction: string, amount: bigint, paymentAccountId: string|undefined) {
// maker creates ethereum payment account
// create payment account if not given
if (!paymentAccountId) {
let testAccount = TEST_CRYPTO_ACCOUNTS[0];
let ethPaymentAccount: PaymentAccount = await maker.createCryptoPaymentAccount(
testAccount.currencyCode + " " + testAccount.address.substr(0, 8) + "... " + GenUtils.getUUID(),
testAccount.currencyCode,
testAccount.address);
paymentAccountId = ethPaymentAccount.getId();
}
// get unlocked balance before reserving offer
let unlockedBalanceBefore: bigint = BigInt((await maker.getBalances()).getUnlockedBalance());
@ -527,7 +664,7 @@ async function postOffer(maker: HavenoDaemon, direction: string, amount: bigint)
amount, // amount
BigInt("150000000000"), // min amount
0.15, // buyer security deposit, e.g. 15%
ethPaymentAccount.getId(), // payment account id
paymentAccountId, // payment account id
undefined); // trigger price // TODO: fails if there is a decimal, gets converted to long?
testOffer(offer);

View File

@ -1,7 +1,9 @@
import {HavenoUtils} from "./HavenoUtils";
import * as grpcWeb from 'grpc-web';
import {GetVersionClient, PriceClient, WalletsClient, OffersClient, PaymentAccountsClient, TradesClient} from './protobuf/GrpcServiceClientPb';
import {GetVersionRequest, GetVersionReply, MarketPriceRequest, MarketPriceReply, MarketPricesRequest, MarketPricesReply, MarketPriceInfo, GetBalancesRequest, GetBalancesReply, XmrBalanceInfo, GetOffersRequest, GetOffersReply, OfferInfo, GetPaymentAccountsRequest, GetPaymentAccountsReply, CreateCryptoCurrencyPaymentAccountRequest, CreateCryptoCurrencyPaymentAccountReply, CreateOfferRequest, CreateOfferReply, CancelOfferRequest, TakeOfferRequest, TakeOfferReply, TradeInfo, GetTradeRequest, GetTradeReply, GetNewDepositSubaddressRequest, GetNewDepositSubaddressReply, ConfirmPaymentStartedRequest, ConfirmPaymentReceivedRequest} from './protobuf/grpc_pb';
import {PaymentAccount, AvailabilityResult} from './protobuf/pb_pb';
const console = require('console');
/**
* Haveno daemon client using gRPC.
@ -11,6 +13,7 @@ class HavenoDaemon {
// instance variables
_url: string;
_password: string;
_process: any;
_getVersionClient: GetVersionClient;
_priceClient: PriceClient;
_walletsClient: WalletsClient;
@ -22,9 +25,12 @@ class HavenoDaemon {
* Construct a client connected to a Haveno daemon.
*
* @param {string} url - Haveno daemon url
* @param {string} password - Haveno daemon password if applicable
* @param {string} password - Haveno daemon password
*/
constructor(url: string, password: string) {
HavenoUtils.log(1, "Creating HavenoDaemon(" + url + ", " + password + ")");
if (!url) throw new Error("Must provide URL of Haveno daemon");
if (!password) throw new Error("Must provide password of Haveno daemon");
this._url = url;
this._password = password;
this._getVersionClient = new GetVersionClient(this._url);
@ -35,6 +41,101 @@ class HavenoDaemon {
this._tradesClient = new TradesClient(this._url);
}
/**
* Start a new Haveno process.
*
* @param {string} havenoPath - path to Haveno binaries
* @param {string[]} cmd - command to start the process
* @param {string} url - Haveno daemon url (must proxy to api port)
* @return {HavenoDaemon} a client connected to the newly started Haveno process
*/
static async startProcess(havenoPath: string, cmd: string[], url: string): Promise<HavenoDaemon> {
HavenoUtils.log(1, "Starting Haveno process: " + cmd);
// start process
let process = require('child_process').spawn(cmd[0], cmd.slice(1), {cwd: havenoPath});
process.stdout.setEncoding('utf8');
process.stderr.setEncoding('utf8');
// return promise which resolves after starting havenod
let output = "";
let isResolved = false;
return new Promise(function(resolve, reject) {
// handle stdout
process.stdout.on('data', async function(data: any) {
let line = data.toString();
HavenoUtils.log(2, line);
output += line + '\n'; // capture output in case of error
// read success message
if (line.indexOf("initDomainServices") >= 0) {
// get api password
let passwordIdx = cmd.indexOf("--apiPassword");
if (passwordIdx < 0) {
reject("Must provide API password to start Haveno daemon");
return;
}
let password = cmd[passwordIdx + 1];
// create client connected to internal process
let daemon = new HavenoDaemon(url, password);
daemon._process = process;
// resolve promise with client connected to internal process
isResolved = true;
resolve(daemon);
}
});
// handle stderr
process.stderr.on('data', function(data: any) {
if (HavenoUtils.getLogLevel() >= 2) console.error(data);
});
// handle exit
process.on("exit", function(code: any) {
if (!isResolved) reject(new Error("Haveno process terminated with exit code " + code + (output ? ":\n\n" + output : "")));
});
// handle error
process.on("error", function(err: any) {
if (err.message.indexOf("ENOENT") >= 0) reject(new Error("haveno-daemon does not exist at path '" + cmd[0] + "'"));
if (!isResolved) reject(err);
});
// handle uncaught exception
process.on("uncaughtException", function(err: any, origin: any) {
console.error("Uncaught exception in Haveno process: " + err.message);
console.error(origin);
reject(err);
});
});
}
/**
* Stop a previously started Haveno process.
*/
async stopProcess(): Promise<void> {
if (this._process === undefined) throw new Error("HavenoDaemon instance not created from new process");
let that = this;
return new Promise(function(resolve, reject) {
that._process.on("exit", function() { resolve(); });
that._process.on("error", function(err: any) { reject(err); });
that._process.kill("SIGINT");
});
}
/**
* Get the URL of the Haveno daemon.
*
* @return {string} the URL of the Haveno daemon
*/
getUrl(): string {
return this._url;
}
/**
* Get the Haveno version.
*
@ -198,7 +299,7 @@ class HavenoDaemon {
* @param {number} buyerSecurityDeposit - buyer security deposit as % of trade amount
* @param {string} paymentAccountId - payment account id
* @param {number} triggerPrice - price to remove offer
* @return {OfferInfo} the created offer
* @return {OfferInfo} the posted offer
*/
async postOffer(currencyCode: string,
direction: string,

42
src/HavenoUtils.ts Normal file
View File

@ -0,0 +1,42 @@
const assert = require("assert");
const console = require('console');
/**
* Collection of utilities for working with Haveno.
*/
class HavenoUtils {
static LOG_LEVEL = 0;
/**
* Log a message.
*
* @param {int} level - log level of the message
* @param {string} msg - message to log
*/
static log(level: number, msg: string) {
assert(level === parseInt(level + "", 10) && level >= 0, "Log level must be an integer >= 0");
if (HavenoUtils.LOG_LEVEL >= level) process.stdout.write(msg);
}
/**
* Set the log level with 0 being least verbose.
*
* @param {int} level - the log level
*/
static async setLogLevel(level: number) {
assert(level === parseInt(level + "", 10) && level >= 0, "Log level must be an integer >= 0");
HavenoUtils.LOG_LEVEL = level;
}
/**
* Get the log level.
*
* @return {int} the current log level
*/
static getLogLevel(): number {
return HavenoUtils.LOG_LEVEL;
}
}
export {HavenoUtils};