mirror of
https://github.com/haveno-dex/haveno-ts.git
synced 2024-12-26 07:49:37 -05:00
test unexpected errors and timeouts during trade initialization
improve logging
This commit is contained in:
parent
fe2c715d80
commit
0d179da988
@ -16,11 +16,13 @@ const TaskLooper = monerojs.TaskLooper;
|
|||||||
// other required imports
|
// other required imports
|
||||||
const console = require('console'); // import console because jest swallows messages in real time
|
const console = require('console'); // import console because jest swallows messages in real time
|
||||||
const assert = require("assert");
|
const assert = require("assert");
|
||||||
|
const net = require('net');
|
||||||
|
|
||||||
// --------------------------- TEST CONFIGURATION -----------------------------
|
// --------------------------- TEST CONFIGURATION -----------------------------
|
||||||
|
|
||||||
// set log level (gets more verbose increasing from 0)
|
// logging options
|
||||||
HavenoUtils.setLogLevel(0);
|
HavenoUtils.setLogLevel(1); // set log level (gets more verbose increasing from 0)
|
||||||
|
const LOG_PROCESS_OUTPUT = false; // enable or disable logging process output
|
||||||
|
|
||||||
// path to directory with haveno binaries
|
// path to directory with haveno binaries
|
||||||
const HAVENO_PATH = "../haveno";
|
const HAVENO_PATH = "../haveno";
|
||||||
@ -46,7 +48,7 @@ let aliceWallet: any;
|
|||||||
// bob config
|
// bob config
|
||||||
const BOB_DAEMON_URL = "http://localhost:8081";
|
const BOB_DAEMON_URL = "http://localhost:8081";
|
||||||
const BOB_DAEMON_PASSWORD = "apitest";
|
const BOB_DAEMON_PASSWORD = "apitest";
|
||||||
let bob: HavenoDaemon = new HavenoDaemon(BOB_DAEMON_URL, BOB_DAEMON_PASSWORD);
|
let bob: HavenoDaemon;
|
||||||
|
|
||||||
// monero daemon config
|
// monero daemon config
|
||||||
const MONERO_DAEMON_URL = "http://localhost:38081"
|
const MONERO_DAEMON_URL = "http://localhost:38081"
|
||||||
@ -82,6 +84,7 @@ const PROXY_PORTS = new Map<string, string[]>([
|
|||||||
|
|
||||||
// track started haveno processes
|
// track started haveno processes
|
||||||
const HAVENO_PROCESSES: HavenoDaemon[] = [];
|
const HAVENO_PROCESSES: HavenoDaemon[] = [];
|
||||||
|
const HAVENO_PROCESS_PORTS: string[] = [];
|
||||||
|
|
||||||
// ----------------------------------- TESTS ----------------------------------
|
// ----------------------------------- TESTS ----------------------------------
|
||||||
|
|
||||||
@ -103,7 +106,7 @@ beforeAll(async () => {
|
|||||||
//console.log((await bob.getBalances()).getUnlockedBalance() + ", " + (await bob.getBalances()).getLockedBalance());
|
//console.log((await bob.getBalances()).getUnlockedBalance() + ", " + (await bob.getBalances()).getLockedBalance());
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.setTimeout(300000);
|
jest.setTimeout(400000);
|
||||||
test("Can get the version", async () => {
|
test("Can get the version", async () => {
|
||||||
let version = await alice.getVersion();
|
let version = await alice.getVersion();
|
||||||
expect(version).toEqual(HAVENO_VERSION);
|
expect(version).toEqual(HAVENO_VERSION);
|
||||||
@ -225,6 +228,11 @@ test("Can post and remove an offer", async () => {
|
|||||||
|
|
||||||
// post offer
|
// post offer
|
||||||
let offer: OfferInfo = await postOffer(alice, "buy", BigInt("200000000000"), undefined);
|
let offer: OfferInfo = await postOffer(alice, "buy", BigInt("200000000000"), undefined);
|
||||||
|
assert.equal(offer.getState(), "AVAILABLE");
|
||||||
|
|
||||||
|
// has offer
|
||||||
|
offer = await alice.getMyOffer(offer.getId());
|
||||||
|
assert.equal(offer.getState(), "AVAILABLE");
|
||||||
|
|
||||||
// cancel offer
|
// cancel offer
|
||||||
await alice.removeOffer(offer.getId());
|
await alice.removeOffer(offer.getId());
|
||||||
@ -237,8 +245,10 @@ test("Can post and remove an offer", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Invalidates offers when reserved funds are spent", async () => {
|
test("Invalidates offers when reserved funds are spent", async () => {
|
||||||
|
let err;
|
||||||
// wait for alice and bob to have unlocked balance for trade
|
let tx;
|
||||||
|
try {
|
||||||
|
// wait for alice to have unlocked balance for trade
|
||||||
let tradeAmount: bigint = BigInt("250000000000");
|
let tradeAmount: bigint = BigInt("250000000000");
|
||||||
await waitForUnlockedBalance(tradeAmount * BigInt("2"), alice);
|
await waitForUnlockedBalance(tradeAmount * BigInt("2"), alice);
|
||||||
|
|
||||||
@ -265,7 +275,7 @@ test("Invalidates offers when reserved funds are spent", async () => {
|
|||||||
// spend one of offer's reserved outputs
|
// spend one of offer's reserved outputs
|
||||||
if (!reservedKeyImages.length) throw new Error("No reserved key images detected");
|
if (!reservedKeyImages.length) throw new Error("No reserved key images detected");
|
||||||
await aliceWallet.thawOutput(reservedKeyImages[0]);
|
await aliceWallet.thawOutput(reservedKeyImages[0]);
|
||||||
let tx = await aliceWallet.sweepOutput({keyImage: reservedKeyImages[0], address: await aliceWallet.getPrimaryAddress(), relay: false});
|
tx = await aliceWallet.sweepOutput({keyImage: reservedKeyImages[0], address: await aliceWallet.getPrimaryAddress(), relay: false});
|
||||||
await monerod.submitTxHex(tx.getFullHex(), true);
|
await monerod.submitTxHex(tx.getFullHex(), true);
|
||||||
|
|
||||||
// wait for spend to be seen
|
// wait for spend to be seen
|
||||||
@ -284,9 +294,108 @@ test("Invalidates offers when reserved funds are spent", async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.message === "cannot remove invalidated offer") throw new Error(err.message);
|
if (err.message === "cannot remove invalidated offer") throw new Error(err.message);
|
||||||
}
|
}
|
||||||
|
} catch (err2) {
|
||||||
|
err = err2;
|
||||||
|
}
|
||||||
|
|
||||||
// flush tx from pool
|
// flush tx from pool
|
||||||
await monerod.flushTxPool(tx.getHash());
|
if (tx) await monerod.flushTxPool(tx.getHash());
|
||||||
|
if (err) throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO (woodser): test arbitrator state too
|
||||||
|
// TODO (woodser): test breaking protocol after depositing to multisig (e.g. don't send payment account payload by deleting it)
|
||||||
|
test("Handles unexpected errors during trade initialization", async () => {
|
||||||
|
let traders: HavenoDaemon[] = [];
|
||||||
|
let err: any;
|
||||||
|
try {
|
||||||
|
|
||||||
|
// start and fund 3 trader processes
|
||||||
|
let tradeAmount: bigint = BigInt("250000000000");
|
||||||
|
console.log("Starting trader processes");
|
||||||
|
traders = await startTraderProcesses(3, LOG_PROCESS_OUTPUT);
|
||||||
|
await traders[0].getBalances();
|
||||||
|
await waitForUnlockedBalance(tradeAmount * BigInt("2"), traders[0], traders[1], traders[2]);
|
||||||
|
|
||||||
|
// trader 0 posts offer
|
||||||
|
console.log("Posting offer");
|
||||||
|
let offer = await postOffer(traders[0], "buy", tradeAmount, undefined);
|
||||||
|
offer = await traders[0].getMyOffer(offer.getId());
|
||||||
|
assert.equal(offer.getState(), "AVAILABLE");
|
||||||
|
|
||||||
|
// wait for offer for offer to be seen
|
||||||
|
await wait(WALLET_SYNC_PERIOD * 2);
|
||||||
|
|
||||||
|
// trader 1 spends trade funds after initializing trade
|
||||||
|
let paymentAccount = await createCryptoPaymentAccount(traders[1]);
|
||||||
|
wait(3000).then(async function() {
|
||||||
|
try {
|
||||||
|
let traderWallet = await monerojs.connectToWalletRpc("http://localhost:" + traders[1].getWalletRpcPort(), "rpc_user", "abc123"); // TODO: don't hardcode here, protect wallet rpc based on account password
|
||||||
|
for (let frozenOutput of await traderWallet.getOutputs({isFrozen: true})) await traderWallet.thawOutput(frozenOutput.getKeyImage().getHex());
|
||||||
|
console.log("Sweeping trade funds");
|
||||||
|
await traderWallet.sweepUnlocked({address: await traderWallet.getPrimaryAddress(), relay: true});
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Caught error sweeping funds!");
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// trader 1 tries to take offer
|
||||||
|
try {
|
||||||
|
console.log("Trader 1 taking offer");
|
||||||
|
await traders[1].takeOffer(offer.getId(), paymentAccount.getId());
|
||||||
|
throw new Error("Should have failed taking offer because taker trade funds spent")
|
||||||
|
} catch (err) {
|
||||||
|
assert(err.message.includes("not enough money"), "Unexpected error: " + err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: test that unavailable right after taking (taker will know before maker)
|
||||||
|
|
||||||
|
// trader 0's offer remains available
|
||||||
|
await wait(10000); // give time for trade initialization to fail and offer to become available
|
||||||
|
offer = await traders[0].getMyOffer(offer.getId());
|
||||||
|
if (offer.getState() !== "AVAILABLE") {
|
||||||
|
console.log("Offer is not yet available, waiting to become available after timeout..."); // there is no error notice if peer stops responding
|
||||||
|
await wait(25000); // give another 25 seconds to become available
|
||||||
|
offer = await traders[0].getMyOffer(offer.getId());
|
||||||
|
assert.equal(offer.getState(), "AVAILABLE");
|
||||||
|
}
|
||||||
|
|
||||||
|
// trader 0 spends trade funds then trader 2 takes offer
|
||||||
|
wait(3000).then(async function() {
|
||||||
|
try {
|
||||||
|
let traderWallet = await monerojs.connectToWalletRpc("http://localhost:" + traders[0].getWalletRpcPort(), "rpc_user", "abc123"); // TODO: don't hardcode here, protect wallet rpc based on account password
|
||||||
|
for (let frozenOutput of await traderWallet.getOutputs({isFrozen: true})) await traderWallet.thawOutput(frozenOutput.getKeyImage().getHex());
|
||||||
|
console.log("Sweeping offer funds");
|
||||||
|
await traderWallet.sweepUnlocked({address: await traderWallet.getPrimaryAddress(), relay: true});
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Caught error sweeping funds!");
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// trader 2 tries to take offer
|
||||||
|
paymentAccount = await createCryptoPaymentAccount(traders[2]);
|
||||||
|
try {
|
||||||
|
console.log("Trader 2 taking offer")
|
||||||
|
await traders[2].takeOffer(offer.getId(), paymentAccount.getId());
|
||||||
|
throw new Error("Should have failed taking offer because maker trade funds spent")
|
||||||
|
} catch (err) {
|
||||||
|
assert(err.message.includes("not enough money") || err.message.includes("timeout reached. protocol did not complete"), "Unexpected error: " + err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// trader 2's balance is unreserved
|
||||||
|
let trader2Balances = await traders[2].getBalances();
|
||||||
|
expect(BigInt(trader2Balances.getReservedTradeBalance())).toEqual(BigInt("0"));
|
||||||
|
expect(BigInt(trader2Balances.getUnlockedBalance())).toBeGreaterThan(BigInt("0"));
|
||||||
|
} catch (err2) {
|
||||||
|
err = err2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop traders
|
||||||
|
console.log("Stopping haveno processes");
|
||||||
|
for (let trader of traders) await stopHavenoProcess(trader);
|
||||||
|
if (err) throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Cannot make or take offer with insufficient unlocked funds", async () => {
|
test("Cannot make or take offer with insufficient unlocked funds", async () => {
|
||||||
@ -295,18 +404,14 @@ test("Cannot make or take offer with insufficient unlocked funds", async () => {
|
|||||||
try {
|
try {
|
||||||
|
|
||||||
// start charlie
|
// start charlie
|
||||||
charlie = await startTraderProcess();
|
charlie = await startTraderProcess(LOG_PROCESS_OUTPUT);
|
||||||
|
|
||||||
// charlie creates ethereum payment account
|
// charlie creates ethereum payment account
|
||||||
let testAccount = TEST_CRYPTO_ACCOUNTS[0];
|
let paymentAccount = await createCryptoPaymentAccount(charlie);
|
||||||
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
|
// charlie cannot make offer with insufficient funds
|
||||||
try {
|
try {
|
||||||
await postOffer(charlie, "buy", BigInt("200000000000"), ethPaymentAccount.getId());
|
await postOffer(charlie, "buy", BigInt("200000000000"), paymentAccount.getId());
|
||||||
throw new Error("Should have failed making offer with insufficient funds")
|
throw new Error("Should have failed making offer with insufficient funds")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let errTyped = err as grpcWeb.RpcError;
|
let errTyped = err as grpcWeb.RpcError;
|
||||||
@ -323,16 +428,17 @@ test("Cannot make or take offer with insufficient unlocked funds", async () => {
|
|||||||
await waitForUnlockedBalance(tradeAmount * BigInt("2"), alice);
|
await waitForUnlockedBalance(tradeAmount * BigInt("2"), alice);
|
||||||
offer = await postOffer(alice, "buy", tradeAmount, undefined);
|
offer = await postOffer(alice, "buy", tradeAmount, undefined);
|
||||||
assert.equal(offer.getState(), "AVAILABLE");
|
assert.equal(offer.getState(), "AVAILABLE");
|
||||||
|
await wait(WALLET_SYNC_PERIOD * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// charlie cannot take offer with insufficient funds
|
// charlie cannot take offer with insufficient funds
|
||||||
try {
|
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
|
await charlie.takeOffer(offer.getId(), paymentAccount.getId()); // TODO (woodser): this returns before trade is fully initialized
|
||||||
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.equal(errTyped.code, 2);
|
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
|
// charlie does not have trade
|
||||||
@ -369,11 +475,25 @@ test("Can complete a trade", async () => {
|
|||||||
expect(offer.getState()).toEqual("AVAILABLE");
|
expect(offer.getState()).toEqual("AVAILABLE");
|
||||||
console.log("Alice done posting offer");
|
console.log("Alice done posting offer");
|
||||||
|
|
||||||
|
// TODO (woodser): test error message taking offer before posted
|
||||||
|
|
||||||
// bob sees offer
|
// bob sees offer
|
||||||
await wait(WALLET_SYNC_PERIOD * 2);
|
await wait(WALLET_SYNC_PERIOD * 2);
|
||||||
let offerBob = getOffer(await bob.getOffers(direction), offer.getId());
|
let offerBob = getOffer(await bob.getOffers(direction), offer.getId());
|
||||||
if (!offerBob) throw new Error("Offer " + offer.getId() + " was not found in peer's offers after posting");
|
if (!offerBob) throw new Error("Offer " + offer.getId() + " was not found in peer's offers after posting");
|
||||||
expect(offerBob.getState()).toEqual("UNKNOWN");
|
expect(offerBob.getState()).toEqual("UNKNOWN"); // TODO: offer state is not known?
|
||||||
|
|
||||||
|
// cannot take offer with invalid payment id
|
||||||
|
let aliceTradesBefore = await alice.getTrades();
|
||||||
|
let bobTradesBefore = await bob.getTrades();
|
||||||
|
try {
|
||||||
|
await bob.takeOffer(offer.getId(), "abc");
|
||||||
|
throw new Error("taking offer with invalid payment account id should fail");
|
||||||
|
} catch (err) {
|
||||||
|
assert.equal(err.message, "payment account with id 'abc' not found");
|
||||||
|
assert.equal((await alice.getTrades()).length, aliceTradesBefore.length, "alice should have not new trades");
|
||||||
|
assert.equal((await bob.getTrades()).length, bobTradesBefore.length, "bob should not have new trades"); // TODO (woodser): also test balance unreserved
|
||||||
|
}
|
||||||
|
|
||||||
// bob creates ethereum payment account
|
// bob creates ethereum payment account
|
||||||
let testAccount = TEST_CRYPTO_ACCOUNTS[0];
|
let testAccount = TEST_CRYPTO_ACCOUNTS[0];
|
||||||
@ -385,7 +505,7 @@ test("Can complete a trade", async () => {
|
|||||||
// bob takes offer
|
// bob takes offer
|
||||||
let startTime = Date.now();
|
let startTime = Date.now();
|
||||||
console.log("Bob taking offer");
|
console.log("Bob taking offer");
|
||||||
let trade: TradeInfo = await bob.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
|
let trade: TradeInfo = await bob.takeOffer(offer.getId(), ethPaymentAccount.getId()); // TODO (woodser): this returns before trade is fully initialized
|
||||||
expect(trade.getPhase()).toEqual("DEPOSIT_PUBLISHED");
|
expect(trade.getPhase()).toEqual("DEPOSIT_PUBLISHED");
|
||||||
console.log("Bob done taking offer in " + (Date.now() - startTime) + " ms");
|
console.log("Bob done taking offer in " + (Date.now() - startTime) + " ms");
|
||||||
|
|
||||||
@ -456,25 +576,33 @@ test("Can complete a trade", async () => {
|
|||||||
// ------------------------------- HELPERS ------------------------------------
|
// ------------------------------- HELPERS ------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a Haveno trader process.
|
* Start Haveno trader 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 startTraderProcesses(numProcesses: number, enableLogging: boolean): Promise<HavenoDaemon[]> {
|
||||||
|
let traderPromises: Promise<HavenoDaemon>[] = [];
|
||||||
|
for (let i = 0; i < numProcesses; i++) traderPromises.push(startTraderProcess(enableLogging));
|
||||||
|
return Promise.all(traderPromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a Haveno trader daemon as a process.
|
||||||
|
*
|
||||||
|
* @param {boolean} enableLogging - specifies if process output should be logged
|
||||||
* @return {HavenoDaemon} the client connected to the started Haveno process
|
* @return {HavenoDaemon} the client connected to the started Haveno process
|
||||||
*/
|
*/
|
||||||
async function startTraderProcess(): Promise<HavenoDaemon> {
|
async function startTraderProcess(enableLogging: boolean): Promise<HavenoDaemon> {
|
||||||
|
|
||||||
// iterate to find unused proxy port
|
// iterate to find unused proxy port
|
||||||
for (let port of Array.from(PROXY_PORTS.keys())) {
|
for (let port of Array.from(PROXY_PORTS.keys())) {
|
||||||
if (port === "8080" || port === "8081") continue; // reserved for alice and bob
|
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
|
// start haveno process on unused port
|
||||||
if (!used) {
|
if (!GenUtils.arrayContains(HAVENO_PROCESS_PORTS, port)) {
|
||||||
|
HAVENO_PROCESS_PORTS.push(port);
|
||||||
let appName = "haveno-XMR_STAGENET_trader_" + GenUtils.getUUID();
|
let appName = "haveno-XMR_STAGENET_trader_" + GenUtils.getUUID();
|
||||||
let cmd: string[] = [
|
let cmd: string[] = [
|
||||||
"./haveno-daemon",
|
"./haveno-daemon",
|
||||||
@ -484,9 +612,10 @@ async function startTraderProcess(): Promise<HavenoDaemon> {
|
|||||||
"--nodePort", PROXY_PORTS.get(port)![1],
|
"--nodePort", PROXY_PORTS.get(port)![1],
|
||||||
"--appName", appName,
|
"--appName", appName,
|
||||||
"--apiPassword", "apitest",
|
"--apiPassword", "apitest",
|
||||||
"--apiPort", PROXY_PORTS.get(port)![0]
|
"--apiPort", PROXY_PORTS.get(port)![0],
|
||||||
|
"--walletRpcBindPort", await getFreePort() + ""
|
||||||
];
|
];
|
||||||
let havenod = await HavenoDaemon.startProcess(HAVENO_PATH, cmd, "http://localhost:" + port);
|
let havenod = await HavenoDaemon.startProcess(HAVENO_PATH, cmd, "http://localhost:" + port, enableLogging);
|
||||||
HAVENO_PROCESSES.push(havenod);
|
HAVENO_PROCESSES.push(havenod);
|
||||||
return havenod;
|
return havenod;
|
||||||
}
|
}
|
||||||
@ -495,11 +624,27 @@ async function startTraderProcess(): Promise<HavenoDaemon> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop a Haveno trader process and release its ports for reuse.
|
* Get a free port.
|
||||||
|
*/
|
||||||
|
async function getFreePort(): Promise<number> {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
let srv = net.createServer();
|
||||||
|
srv.listen(0, function() {
|
||||||
|
let port = srv.address().port;
|
||||||
|
srv.close(function() {
|
||||||
|
resolve(port);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop a Haveno daemon process and release its ports for reuse.
|
||||||
*/
|
*/
|
||||||
async function stopHavenoProcess(havenod: HavenoDaemon) {
|
async function stopHavenoProcess(havenod: HavenoDaemon) {
|
||||||
await havenod.stopProcess();
|
await havenod.stopProcess();
|
||||||
GenUtils.remove(HAVENO_PROCESSES, havenod);
|
GenUtils.remove(HAVENO_PROCESSES, havenod);
|
||||||
|
GenUtils.remove(HAVENO_PROCESS_PORTS, new URL(havenod.getUrl()).port);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -564,7 +709,7 @@ async function waitForUnlockedBalance(amount: bigint, ...wallets: any[]) {
|
|||||||
|
|
||||||
async getDepositAddress(): Promise<string> {
|
async getDepositAddress(): Promise<string> {
|
||||||
if (this._wallet instanceof HavenoDaemon) return await this._wallet.getNewDepositSubaddress();
|
if (this._wallet instanceof HavenoDaemon) return await this._wallet.getNewDepositSubaddress();
|
||||||
else return await this._wallet.getPrimaryAddress();
|
else return (await this._wallet.createSubaddress()).getAddress();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -592,7 +737,7 @@ async function waitForUnlockedBalance(amount: bigint, ...wallets: any[]) {
|
|||||||
// wait for funds to unlock
|
// wait for funds to unlock
|
||||||
console.log("Mining for unlocked balance of " + amount);
|
console.log("Mining for unlocked balance of " + amount);
|
||||||
await startMining();
|
await startMining();
|
||||||
let promises: Promise<void>[] = []
|
let promises: Promise<void>[] = [];
|
||||||
for (let wallet of wallets) {
|
for (let wallet of wallets) {
|
||||||
promises.push(new Promise(async function(resolve, reject) {
|
promises.push(new Promise(async function(resolve, reject) {
|
||||||
let taskLooper: any = new TaskLooper(async function() {
|
let taskLooper: any = new TaskLooper(async function() {
|
||||||
@ -640,18 +785,20 @@ async function wait(durationMs: number) {
|
|||||||
return new Promise(function(resolve) { setTimeout(resolve, durationMs); });
|
return new Promise(function(resolve) { setTimeout(resolve, durationMs); });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postOffer(maker: HavenoDaemon, direction: string, amount: bigint, paymentAccountId: string|undefined) {
|
async function createCryptoPaymentAccount(trader: HavenoDaemon): Promise<PaymentAccount> {
|
||||||
|
|
||||||
// create payment account if not given
|
|
||||||
if (!paymentAccountId) {
|
|
||||||
let testAccount = TEST_CRYPTO_ACCOUNTS[0];
|
let testAccount = TEST_CRYPTO_ACCOUNTS[0];
|
||||||
let ethPaymentAccount: PaymentAccount = await maker.createCryptoPaymentAccount(
|
let paymentAccount: PaymentAccount = await trader.createCryptoPaymentAccount(
|
||||||
testAccount.currencyCode + " " + testAccount.address.substr(0, 8) + "... " + GenUtils.getUUID(),
|
testAccount.currencyCode + " " + testAccount.address.substr(0, 8) + "... " + GenUtils.getUUID(),
|
||||||
testAccount.currencyCode,
|
testAccount.currencyCode,
|
||||||
testAccount.address);
|
testAccount.address);
|
||||||
paymentAccountId = ethPaymentAccount.getId();
|
return paymentAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function postOffer(maker: HavenoDaemon, direction: string, amount: bigint, paymentAccountId: string|undefined) {
|
||||||
|
|
||||||
|
// create payment account if not given
|
||||||
|
if (!paymentAccountId) paymentAccountId = (await createCryptoPaymentAccount(maker)).getId();
|
||||||
|
|
||||||
// get unlocked balance before reserving offer
|
// get unlocked balance before reserving offer
|
||||||
let unlockedBalanceBefore: bigint = BigInt((await maker.getBalances()).getUnlockedBalance());
|
let unlockedBalanceBefore: bigint = BigInt((await maker.getBalances()).getUnlockedBalance());
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {HavenoUtils} from "./HavenoUtils";
|
import {HavenoUtils} from "./HavenoUtils";
|
||||||
import * as grpcWeb from 'grpc-web';
|
import * as grpcWeb from 'grpc-web';
|
||||||
import {GetVersionClient, PriceClient, WalletsClient, OffersClient, PaymentAccountsClient, TradesClient} from './protobuf/GrpcServiceClientPb';
|
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 {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, GetTradesRequest, GetTradesReply, GetNewDepositSubaddressRequest, GetNewDepositSubaddressReply, ConfirmPaymentStartedRequest, ConfirmPaymentReceivedRequest} from './protobuf/grpc_pb';
|
||||||
import {PaymentAccount, AvailabilityResult} from './protobuf/pb_pb';
|
import {PaymentAccount, AvailabilityResult} from './protobuf/pb_pb';
|
||||||
const console = require('console');
|
const console = require('console');
|
||||||
|
|
||||||
@ -14,6 +14,8 @@ class HavenoDaemon {
|
|||||||
_url: string;
|
_url: string;
|
||||||
_password: string;
|
_password: string;
|
||||||
_process: any;
|
_process: any;
|
||||||
|
_processLogging: boolean = false;
|
||||||
|
_walletRpcPort: number|undefined;
|
||||||
_getVersionClient: GetVersionClient;
|
_getVersionClient: GetVersionClient;
|
||||||
_priceClient: PriceClient;
|
_priceClient: PriceClient;
|
||||||
_walletsClient: WalletsClient;
|
_walletsClient: WalletsClient;
|
||||||
@ -28,7 +30,7 @@ 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(1, "Creating HavenoDaemon(" + url + ", " + password + ")");
|
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");
|
||||||
this._url = url;
|
this._url = url;
|
||||||
@ -47,29 +49,33 @@ class HavenoDaemon {
|
|||||||
* @param {string} havenoPath - path to Haveno binaries
|
* @param {string} havenoPath - path to Haveno binaries
|
||||||
* @param {string[]} cmd - command to start the process
|
* @param {string[]} cmd - command to start the process
|
||||||
* @param {string} url - Haveno daemon url (must proxy to api port)
|
* @param {string} url - Haveno daemon url (must proxy to api port)
|
||||||
|
* @param {boolean} enableLogging - specifies if logging is enabled or disabled at log level 3
|
||||||
* @return {HavenoDaemon} a client connected to the newly started Haveno process
|
* @return {HavenoDaemon} a client connected to the newly started Haveno process
|
||||||
*/
|
*/
|
||||||
static async startProcess(havenoPath: string, cmd: string[], url: string): Promise<HavenoDaemon> {
|
static async startProcess(havenoPath: string, cmd: string[], url: string, enableLogging: boolean): 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
|
// return promise which resolves after starting havenod
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
HavenoUtils.log(2, "Starting Haveno process: " + cmd + " on proxy url: " + url);
|
||||||
|
|
||||||
|
// state variables
|
||||||
let output = "";
|
let output = "";
|
||||||
let isResolved = false;
|
let isResolved = false;
|
||||||
return new Promise(function(resolve, reject) {
|
let daemon: HavenoDaemon|undefined = undefined;
|
||||||
|
|
||||||
|
// start process
|
||||||
|
let childProcess = require('child_process').spawn(cmd[0], cmd.slice(1), {cwd: havenoPath});
|
||||||
|
childProcess.stdout.setEncoding('utf8');
|
||||||
|
childProcess.stderr.setEncoding('utf8');
|
||||||
|
|
||||||
// handle stdout
|
// handle stdout
|
||||||
process.stdout.on('data', async function(data: any) {
|
childProcess.stdout.on('data', async function(data: any) {
|
||||||
let line = data.toString();
|
let line = data.toString();
|
||||||
HavenoUtils.log(2, line);
|
if (HavenoUtils.getLogLevel() >= 3 && 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
|
// read success message
|
||||||
if (line.indexOf("initDomainServices") >= 0) {
|
if (line.indexOf("BisqHeadlessAppMain: onSetupComplete") >= 0) { // TODO (woodser): rename class
|
||||||
|
|
||||||
// get api password
|
// get api password
|
||||||
let passwordIdx = cmd.indexOf("--apiPassword");
|
let passwordIdx = cmd.indexOf("--apiPassword");
|
||||||
@ -80,8 +86,13 @@ class HavenoDaemon {
|
|||||||
let password = cmd[passwordIdx + 1];
|
let password = cmd[passwordIdx + 1];
|
||||||
|
|
||||||
// create client connected to internal process
|
// create client connected to internal process
|
||||||
let daemon = new HavenoDaemon(url, password);
|
daemon = new HavenoDaemon(url, password);
|
||||||
daemon._process = process;
|
daemon._process = childProcess;
|
||||||
|
daemon._processLogging = enableLogging;
|
||||||
|
|
||||||
|
// get wallet rpc port
|
||||||
|
let walletRpcPortIdx = cmd.indexOf("--walletRpcBindPort");
|
||||||
|
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;
|
isResolved = true;
|
||||||
@ -90,27 +101,31 @@ class HavenoDaemon {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// handle stderr
|
// handle stderr
|
||||||
process.stderr.on('data', function(data: any) {
|
childProcess.stderr.on('data', function(data: any) {
|
||||||
if (HavenoUtils.getLogLevel() >= 2) console.error(data);
|
if (HavenoUtils.getLogLevel() >= 2 && loggingEnabled()) process.stderr.write(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// handle exit
|
// handle exit
|
||||||
process.on("exit", function(code: any) {
|
childProcess.on("exit", function(code: any) {
|
||||||
if (!isResolved) reject(new Error("Haveno process terminated with exit code " + code + (output ? ":\n\n" + output : "")));
|
if (!isResolved) reject(new Error("Haveno process terminated with exit code " + code + (output ? ":\n\n" + output : "")));
|
||||||
});
|
});
|
||||||
|
|
||||||
// handle error
|
// handle error
|
||||||
process.on("error", function(err: any) {
|
childProcess.on("error", 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) reject(err);
|
if (!isResolved) reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// handle uncaught exception
|
// handle uncaught exception
|
||||||
process.on("uncaughtException", function(err: any, origin: any) {
|
childProcess.on("uncaughtException", 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);
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function loggingEnabled(): boolean {
|
||||||
|
return (daemon && daemon._processLogging) || (!daemon && enableLogging);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,6 +142,16 @@ class HavenoDaemon {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable process logging.
|
||||||
|
*
|
||||||
|
* @param {boolean} enabled - specifies if logging is enabled or disabled
|
||||||
|
*/
|
||||||
|
setProcessLogging(enabled: boolean) {
|
||||||
|
if (this._process === undefined) throw new Error("HavenoDaemon instance not created from new process");
|
||||||
|
this._processLogging = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the URL of the Haveno daemon.
|
* Get the URL of the Haveno daemon.
|
||||||
*
|
*
|
||||||
@ -136,6 +161,15 @@ class HavenoDaemon {
|
|||||||
return this._url;
|
return this._url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the port of the primary wallet rpc instance if known.
|
||||||
|
*
|
||||||
|
* @return {number|undefined} the port of the primary wallet rpc instance if known
|
||||||
|
*/
|
||||||
|
getWalletRpcPort(): number|undefined {
|
||||||
|
return this._walletRpcPort;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Haveno version.
|
* Get the Haveno version.
|
||||||
*
|
*
|
||||||
@ -255,8 +289,7 @@ class HavenoDaemon {
|
|||||||
/**
|
/**
|
||||||
* Get available offers to buy or sell XMR.
|
* Get available offers to buy or sell XMR.
|
||||||
*
|
*
|
||||||
* @param {string} direction - one of "BUY" or "SELL"
|
* @param {string} direction - one of "BUY" or "SELL" // TODO (woodser): make optional
|
||||||
*
|
|
||||||
* @return {OfferInfo[]} available offers
|
* @return {OfferInfo[]} available offers
|
||||||
*/
|
*/
|
||||||
async getOffers(direction: string): Promise<OfferInfo[]> {
|
async getOffers(direction: string): Promise<OfferInfo[]> {
|
||||||
@ -272,8 +305,7 @@ class HavenoDaemon {
|
|||||||
/**
|
/**
|
||||||
* Get user's created offers to buy or sell XMR.
|
* Get user's created offers to buy or sell XMR.
|
||||||
*
|
*
|
||||||
* @param {string} direction - one of "BUY" or "SELL"
|
* @param {string} direction - one of "BUY" or "SELL" // TODO (woodser): make optional
|
||||||
*
|
|
||||||
* @return {OfferInfo[]} the user's created offers
|
* @return {OfferInfo[]} the user's created offers
|
||||||
*/
|
*/
|
||||||
async getMyOffers(direction: string): Promise<OfferInfo[]> {
|
async getMyOffers(direction: string): Promise<OfferInfo[]> {
|
||||||
@ -286,6 +318,21 @@ class HavenoDaemon {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get my offer by id.
|
||||||
|
*
|
||||||
|
* @param {string} offerId - id of the user's created offer
|
||||||
|
* @return {OfferInfo} the user's created offer
|
||||||
|
*/
|
||||||
|
async getMyOffer(offerId: string): Promise<OfferInfo> {
|
||||||
|
// TODO: implement this call on the backend
|
||||||
|
let offers = await this.getMyOffers("buy");
|
||||||
|
for (let offer of offers) if (offer.getId() === offerId) return offer;
|
||||||
|
offers = await this.getMyOffers("sell");
|
||||||
|
for (let offer of offers) if (offer.getId() === offerId) return offer;
|
||||||
|
throw new Error("No offer with id: " + offerId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Post an offer.
|
* Post an offer.
|
||||||
*
|
*
|
||||||
@ -383,6 +430,21 @@ class HavenoDaemon {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all trades.
|
||||||
|
*
|
||||||
|
* @return {TradeInfo[]} all user trades
|
||||||
|
*/
|
||||||
|
async getTrades(): Promise<TradeInfo[]> {
|
||||||
|
let that = this;
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
that._tradesClient.getTrades(new GetTradesRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: GetTradesReply) {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(response.getTradesList());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirm a payment is started.
|
* Confirm a payment is started.
|
||||||
*
|
*
|
||||||
|
@ -16,7 +16,7 @@ class HavenoUtils {
|
|||||||
*/
|
*/
|
||||||
static log(level: number, msg: string) {
|
static log(level: number, msg: string) {
|
||||||
assert(level === parseInt(level + "", 10) && level >= 0, "Log level must be an integer >= 0");
|
assert(level === parseInt(level + "", 10) && level >= 0, "Log level must be an integer >= 0");
|
||||||
if (HavenoUtils.LOG_LEVEL >= level) process.stdout.write(msg);
|
if (HavenoUtils.LOG_LEVEL >= level) console.log(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user