support local, stagenet, and mainnet xmr network configurations (#122)

This commit is contained in:
woodser 2022-07-07 09:11:50 -04:00 committed by GitHub
parent 82e43d5940
commit 7871a303eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 116 additions and 61 deletions

View File

@ -17,7 +17,7 @@
"plugins": ["@typescript-eslint"],
"ignorePatterns": ["node_modules/**", "**/dist/**", "src/protobuf/**"],
"rules": {
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-non-null-assertion": "off",

View File

@ -36,10 +36,10 @@ Running the [API tests](./src/HavenoClient.test.ts) is the best way to develop a
[`HavenoClient.ts`](./src/HavenoClient.ts) provides the client interface to Haveno's backend daemon.
1. [Run a local Haveno test network](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) and then shut down the arbitrator, Alice, and Bob or run them as daemons, e.g. `make alice-daemon`. You may omit the arbitrator registration steps since it is done automatically in the tests.
1. [Run a local Haveno test network](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) and then shut down the arbitrator, Alice, and Bob or run them as daemons, e.g. `make alice-daemon-local`. You may omit the arbitrator registration steps since it's done automatically in the tests.
2. Clone this project to the same parent directory as the haveno project: `git clone https://github.com/haveno-dex/haveno-ts`
3. In a new terminal, start envoy with the config in haveno-ts/config/envoy.test.yaml (change absolute path for your system): `docker run --rm --add-host host.docker.internal:host-gateway -it -v ~/git/haveno-ts/config/envoy.test.yaml:/envoy.test.yaml -p 8079:8079 -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`.
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-local`.
5. Install protobuf compiler v3.19.1 or later for your system:<br>
mac: `brew install protobuf`<br>
linux: `apt install protobuf-compiler`
@ -47,4 +47,4 @@ Running the [API tests](./src/HavenoClient.test.ts) is the best way to develop a
6. Download `protoc-gen-grpc-web` plugin and make executable as [shown here](https://github.com/grpc/grpc-web#code-generator-plugin).
7. `cd haveno-ts`
8. `npm install`
9. `npm test` to run all tests or `npm run test -- -t 'my test'` to run tests by name.
9. `npm run test -- --baseCurrencyNetwork=XMR_LOCAL` to run all tests or `npm run test -- --baseCurrencyNetwork=XMR_LOCAL -t "my test"` to run tests by name.

View File

@ -9,7 +9,7 @@
"prepare": "scripts/build_protobuf.sh",
"pretest": "scripts/build_protobuf.sh",
"build": "./scripts/build_dist.sh",
"test": "jest ",
"test": "jest",
"eslint": "eslint .",
"eslintfix": "eslint src/* --fix",
"typedoc": "typedoc ./src/index.ts --entryPointStrategy expand src/ --exclude **/*.test.ts"

View File

@ -14,49 +14,59 @@ import OnlineStatus = UrlConnection.OnlineStatus;
const monerojs = require("monero-javascript"); // TODO (woodser): support typescript and `npm install @types/monero-javascript` in monero-javascript
const GenUtils = monerojs.GenUtils;
const BigInteger = monerojs.BigInteger;
const MoneroNetworkType = monerojs.MoneroNetworkType;
const MoneroTxConfig = monerojs.MoneroTxConfig;
const MoneroDestination = monerojs.MoneroDestination;
const MoneroUtils = monerojs.MoneroUtils;
const TaskLooper = monerojs.TaskLooper;
// other required imports
// other imports
import fs from "fs";
import path from "path";
import net from "net";
import assert from "assert";
import console from "console"; // import console because jest swallows messages in real time
import * as os from 'os';
// ------------------------------ TEST CONFIG ---------------------------------
enum BaseCurrencyNetwork {
XMR_MAINNET = "XMR_MAINNET",
XMR_STAGENET = "XMR_STAGENET",
XMR_LOCAL = "XMR_LOCAL"
}
const TestConfig = {
logLevel: 1,
logLevel: 3,
baseCurrencyNetwork: getBaseCurrencyNetwork(),
networkType: getBaseCurrencyNetwork() == BaseCurrencyNetwork.XMR_MAINNET ? monerojs.MoneroNetworkType.MAINNET : getBaseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? monerojs.MoneroNetworkType.TESTNET : monerojs.MoneroNetworkType.STAGENET,
moneroBinsDir: "../haveno/.localnet",
testDataDir: "./testdata",
networkType: monerojs.MoneroNetworkType.STAGENET,
haveno: {
path: "../haveno",
version: "1.6.2"
},
monerod: {
url: "http://localhost:38081",
username: "superuser",
password: "abctesting123"
url: "http://localhost:" + getNetworkStartPort() + "8081", // 18081, 28081, 38081 for mainnet, testnet, stagenet respectively
username: "",
password: ""
},
monerod2: {
url: "http://localhost:58081",
username: "superuser",
password: "abctesting123"
password: "abctesting123",
p2pBindPort: "58080",
rpcBindPort: "58081",
zmqRpcBindPort: "58082"
},
fundingWallet: {
url: "http://localhost:38084",
url: "http://localhost:" + getNetworkStartPort() + "8084", // 18084, 28084, 38084 for mainnet, testnet, stagenet respectively
username: "rpc_user",
password: "abc123",
defaultPath: "test_funding_wallet",
defaultPath: "funding_wallet-" + getBaseCurrencyNetwork(),
minimumFunding: BigInt("5000000000000")
},
defaultHavenod: {
logProcessOutput: false, // log output for processes started by tests (except arbitrator, alice, and bob which are configured separately)
logProcessOutput: true, // log output for processes started by tests (except arbitrator, alice, and bob which are configured separately)
apiPassword: "apitest",
walletUsername: "haveno_user",
walletDefaultPassword: "password", // only used if account password not set
@ -65,21 +75,21 @@ const TestConfig = {
autoLogin: true
},
startupHavenods: [{
appName: "haveno-XMR_STAGENET_arbitrator", // arbritrator
logProcessOutput: false,
appName: "haveno-" + getBaseCurrencyNetwork() + "_arbitrator", // arbritrator
logProcessOutput: true,
url: "http://localhost:8079",
accountPasswordRequired: false,
accountPassword: "abctesting123",
}, {
appName: "haveno-XMR_STAGENET_alice", // alice
logProcessOutput: false,
appName: "haveno-" + getBaseCurrencyNetwork() + "_alice", // alice
logProcessOutput: true,
url: "http://localhost:8080",
accountPasswordRequired: false,
accountPassword: "abctesting456",
walletUrl: "http://127.0.0.1:38091",
}, {
appName: "haveno-XMR_STAGENET_bob", // bob
logProcessOutput: false,
appName: "haveno-" + getBaseCurrencyNetwork() + "_bob", // bob
logProcessOutput: true,
url: "http://localhost:8081",
accountPasswordRequired: false,
accountPassword: "abctesting789",
@ -91,6 +101,7 @@ const TestConfig = {
daemonPollPeriodMs: 15000,
maxWalletStartupMs: 10000, // TODO (woodser): make shorter by switching to jni
maxTimePeerNoticeMs: 3000,
maxCpuPct: 0.25,
assetCodes: ["USD", "GBP", "EUR", "ETH", "BTC", "BCH", "LTC", "ZEC"], // primary asset codes
cryptoAddresses: [{
currencyCode: "ETH",
@ -119,9 +130,9 @@ const TestConfig = {
["8085", ["10004", "7780"]],
["8086", ["10005", "7781"]],
]),
devPrivilegePrivKey: "6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a", // from DEV_PRIVILEGE_PRIV_KEY
arbitratorPrivKey: getArbitratorPrivKey(),
tradeInitTimeout: 60000,
timeout: 900000, // timeout in ms for all tests to complete (15 minutes)
testTimeout: getBaseCurrencyNetwork() === BaseCurrencyNetwork.XMR_LOCAL ? 900000 : 3000000, // timeout in ms for each test to complete (15 minutes for private network, 50 minutes for public network)
postOffer: { // default post offer config
direction: "buy", // buy or sell xmr
amount: BigInt("200000000000"), // amount of xmr to trade
@ -172,13 +183,20 @@ const OFFLINE_ERR_MSG = "Http response at 400 or 500 level";
// -------------------------- BEFORE / AFTER TESTS ----------------------------
jest.setTimeout(TestConfig.timeout);
jest.setTimeout(TestConfig.testTimeout);
beforeAll(async () => {
// set log level for tests
HavenoUtils.setLogLevel(TestConfig.logLevel);
// initialize funding wallet
await initFundingWallet();
HavenoUtils.log(0, "Funding wallet balance: " + await fundingWallet.getBalance());
HavenoUtils.log(0, "Funding wallet unlocked balance: " + await fundingWallet.getUnlockedBalance());
const subaddress = await fundingWallet.createSubaddress(0);
HavenoUtils.log(0, "Funding wallet new subaddress: " + subaddress.getAddress());
// start configured haveno daemons
const promises = [];
for (const config of TestConfig.startupHavenods) promises.push(initHaveno(config));
@ -193,16 +211,13 @@ beforeAll(async () => {
bob = startupHavenods[2];
// register arbitrator dispute agent
await arbitrator.registerDisputeAgent("arbitrator", TestConfig.devPrivilegePrivKey);
await arbitrator.registerDisputeAgent("arbitrator", TestConfig.arbitratorPrivKey);
// connect monero clients
monerod = await monerojs.connectToDaemonRpc(TestConfig.monerod.url, TestConfig.monerod.username, TestConfig.monerod.password);
aliceWallet = await monerojs.connectToWalletRpc(TestConfig.startupHavenods[1].walletUrl, TestConfig.defaultHavenod.walletUsername, TestConfig.startupHavenods[1].accountPasswordRequired ? TestConfig.startupHavenods[1].accountPassword : TestConfig.defaultHavenod.walletDefaultPassword);
bobWallet = await monerojs.connectToWalletRpc(TestConfig.startupHavenods[2].walletUrl, TestConfig.defaultHavenod.walletUsername, TestConfig.startupHavenods[2].accountPasswordRequired ? TestConfig.startupHavenods[2].accountPassword : TestConfig.defaultHavenod.walletDefaultPassword);
// initialize funding wallet
await initFundingWallet();
// create test data directory if it doesn't exist
if (!fs.existsSync(TestConfig.testDataDir)) fs.mkdirSync(TestConfig.testDataDir);
});
@ -329,7 +344,6 @@ test("Can manage an account", async () => {
const paymentAccount2 = await charlie.getPaymentAccount(paymentAccount.getId());
testCryptoPaymentAccountsEqual(paymentAccount, paymentAccount2);
} catch (err2) {
console.log(err2);
err = err2;
}
@ -359,16 +373,14 @@ test("Can manage Monero daemon connections", async () => {
charlie = await initHaveno();
// test default connections
const monerodUrl1 = "http://127.0.0.1:38081"; // TODO: (woodser): move to config
const monerodUrl2 = "http://haveno.exchange:38081";
const monerodUrl1 = "http://127.0.0.1:" + getNetworkStartPort() + "8081"; // TODO: (woodser): move to config
let connections: UrlConnection[] = await charlie.getMoneroConnections();
testConnection(getConnection(connections, monerodUrl1)!, monerodUrl1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
testConnection(getConnection(connections, monerodUrl2)!, monerodUrl2, OnlineStatus.UNKNOWN, AuthenticationStatus.NO_AUTHENTICATION, 2);
// test default connection
let connection: UrlConnection | undefined = await charlie.getMoneroConnection();
assert(await charlie.isConnectedToMonero());
testConnection(connection!, monerodUrl1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
testConnection(connection!, monerodUrl1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1); // TODO: should be no authentication?
// add a new connection
const fooBarUrl = "http://foo.bar";
@ -395,12 +407,12 @@ test("Can manage Monero daemon connections", async () => {
"--" + monerojs.MoneroNetworkType.toString(TestConfig.networkType).toLowerCase(),
"--no-igd",
"--hide-my-port",
"--data-dir", TestConfig.moneroBinsDir + "/stagenet/testnode",
"--p2p-bind-port", "58080",
"--rpc-bind-port", "58081",
"--rpc-login", "superuser:abctesting123",
"--zmq-rpc-bind-port", "58082"
"--data-dir", TestConfig.moneroBinsDir + "/" + TestConfig.baseCurrencyNetwork.toLowerCase() + "/testnode",
"--p2p-bind-port", TestConfig.monerod2.p2pBindPort,
"--rpc-bind-port", TestConfig.monerod2.rpcBindPort,
"--no-zmq"
];
if (TestConfig.monerod2.username) cmd.push("--rpc-login", TestConfig.monerod2.username + ":" + TestConfig.monerod2.password);
monerod2 = await monerojs.connectToDaemonRpc(cmd);
// connection is online and not authenticated
@ -448,7 +460,8 @@ test("Can manage Monero daemon connections", async () => {
connection = await charlie.getMoneroConnection();
testConnection(connection!, monerodUrl1, OnlineStatus.ONLINE, AuthenticationStatus.AUTHENTICATED, 1);
// stop checking connection periodically
// stop auto switch and checking connection periodically
await charlie.setAutoSwitch(false);
await charlie.stopCheckingConnection();
// remove current connection
@ -462,7 +475,6 @@ test("Can manage Monero daemon connections", async () => {
await charlie.checkMoneroConnections();
connections = await charlie.getMoneroConnections();
testConnection(getConnection(connections, fooBarUrl)!, fooBarUrl, OnlineStatus.OFFLINE, AuthenticationStatus.NO_AUTHENTICATION, 0);
for (const connection of connections) testConnection(connection!, connection.getUrl(), OnlineStatus.OFFLINE, AuthenticationStatus.NO_AUTHENTICATION);
// set connection to previous url
await charlie.setMoneroConnection(fooBarUrl);
@ -545,7 +557,7 @@ test("Can start and stop a local Monero node", async() => {
// expect successful start with custom settings
const connectionsBefore = await alice.getMoneroConnections();
const settings: MoneroNodeSettings = new MoneroNodeSettings();
const dataDir = TestConfig.moneroBinsDir + "/stagenet/node1";
const dataDir = TestConfig.moneroBinsDir + "/" + TestConfig.baseCurrencyNetwork + "/node1";
const logFile = dataDir + "/test.log";
const p2pPort = 38080;
const rpcPort = 38081;
@ -601,7 +613,7 @@ test("Has a Monero wallet", async () => {
// get primary address
const primaryAddress = await alice.getXmrPrimaryAddress();
await MoneroUtils.validateAddress(primaryAddress, MoneroNetworkType.STAGENET);
await MoneroUtils.validateAddress(primaryAddress, TestConfig.networkType);
// wait for alice to have unlocked balance
const tradeAmount = BigInt("250000000000");
@ -622,7 +634,7 @@ test("Has a Monero wallet", async () => {
// get new subaddresses
for (let i = 0; i < 0; i++) {
const address = await alice.getXmrNewSubaddress();
await MoneroUtils.validateAddress(address, MoneroNetworkType.STAGNET);
await MoneroUtils.validateAddress(address, TestConfig.networkType);
}
// create withdraw tx
@ -796,14 +808,11 @@ test("Can get market depth", async () => {
.toThrow('Currency not found: INVALID_CURRENCY');
});
test("Can register as dispute agents", async () => {
await arbitrator.registerDisputeAgent("arbitrator", TestConfig.devPrivilegePrivKey);
await arbitrator.registerDisputeAgent("mediator", TestConfig.devPrivilegePrivKey);
await arbitrator.registerDisputeAgent("refundagent", TestConfig.devPrivilegePrivKey);
test("Can register as an arbitrator", async () => {
// test bad dispute agent type
try {
await arbitrator.registerDisputeAgent("unsupported type", TestConfig.devPrivilegePrivKey);
await arbitrator.registerDisputeAgent("unsupported type", TestConfig.arbitratorPrivKey);
throw new Error("should have thrown error registering bad type");
} catch (err: any) {
if (err.message !== "unknown dispute agent type 'unsupported type'") throw new Error("Unexpected error: " + err.message);
@ -811,11 +820,14 @@ test("Can register as dispute agents", async () => {
// test bad key
try {
await arbitrator.registerDisputeAgent("arbitrator", "bad key");
await arbitrator.registerDisputeAgent("mediator", "bad key");
throw new Error("should have thrown error registering bad key");
} catch (err: any) {
if (err.message !== "invalid registration key") throw new Error("Unexpected error: " + err.message);
}
// register arbitrator with good key
await arbitrator.registerDisputeAgent("arbitrator", TestConfig.arbitratorPrivKey);
});
test("Can get offers", async () => {
@ -998,7 +1010,7 @@ test("Can prepare for trading", async () => {
// fund wallets
const tradeAmount = BigInt("250000000000");
await fundOutputs([aliceWallet, bobWallet], tradeAmount * BigInt("6"), 4);
await fundOutputs([aliceWallet, bobWallet], tradeAmount * BigInt("2"), 4);
// wait for havenod to observe funds
await wait(TestConfig.walletSyncPeriodMs);
@ -1205,7 +1217,7 @@ test("Can complete a trade", async () => {
HavenoUtils.log(1, "Bob done taking offer in " + (Date.now() - startTime) + " ms");
// alice is notified that offer is taken
await wait(1000);
await wait(TestConfig.maxTimePeerNoticeMs);
const tradeNotifications = getNotifications(aliceNotifications, NotificationMessage.NotificationType.TRADE_UPDATE);
expect(tradeNotifications.length).toBe(1);
expect(tradeNotifications[0].getTrade()!.getPhase()).toEqual("DEPOSIT_PUBLISHED");
@ -1282,7 +1294,7 @@ test("Can resolve disputes", async () => {
// wait for alice and bob to have unlocked balance for trade
const tradeAmount = BigInt("250000000000");
await fundOutputs([aliceWallet, bobWallet], tradeAmount * BigInt("6"), 4);
await fundOutputs([aliceWallet, bobWallet], tradeAmount * BigInt("2"), 4);
// register to receive notifications
const aliceNotifications: NotificationMessage[] = [];
@ -1749,6 +1761,44 @@ test("Handles unexpected errors during trade initialization", async () => {
// ------------------------------- HELPERS ------------------------------------
function getBaseCurrencyNetwork(): BaseCurrencyNetwork {
const str = getBaseCurrencyNetworkStr();
if (str === "XMR_MAINNET") return BaseCurrencyNetwork.XMR_MAINNET;
else if (str === "XMR_STAGENET") return BaseCurrencyNetwork.XMR_STAGENET;
else if (str === "XMR_LOCAL") return BaseCurrencyNetwork.XMR_LOCAL;
else throw new Error("Unhandled base currency network: " + str);
function getBaseCurrencyNetworkStr() {
for (const arg of process.argv) {
if (arg.indexOf("--baseCurrencyNetwork") === 0) {
return arg.substring(arg.indexOf("=") + 1);
}
}
throw new Error("Must provide base currency network, e.g.: `npm run test -- --baseCurrencyNetwork=XMR_LOCAL -t \"my test\"`");
}
}
function getNetworkStartPort() {
switch (getBaseCurrencyNetwork()) {
case BaseCurrencyNetwork.XMR_MAINNET: return 1;
case BaseCurrencyNetwork.XMR_LOCAL: return 2;
case BaseCurrencyNetwork.XMR_STAGENET: return 3;
default: throw new Error("Unhandled base currency network: " + getBaseCurrencyNetwork());
}
}
function getArbitratorPrivKey() {
switch (getBaseCurrencyNetwork()) {
case BaseCurrencyNetwork.XMR_MAINNET:
throw new Error("Cannot get private key for MAINNET");
case BaseCurrencyNetwork.XMR_STAGENET:
return "1aa111f817b7fdaaec1c8d5281a1837cc71c336db09b87cf23344a0a4e3bb2cb";
case BaseCurrencyNetwork.XMR_LOCAL:
return "6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a"; // from DEV_PRIVILEGE_PRIV_KEY
default:
throw new Error("Unhandled base currency network: " + getBaseCurrencyNetwork());
}
}
async function initHavenos(numDaemons: number, config?: any) {
const traderPromises: Promise<HavenoClient>[] = [];
for (let i = 0; i < numDaemons; i++) traderPromises.push(initHaveno(config));
@ -1757,7 +1807,7 @@ async function initHavenos(numDaemons: number, config?: any) {
async function initHaveno(config?: any): Promise<HavenoClient> {
config = Object.assign({}, TestConfig.defaultHavenod, config);
if (!config.appName) config.appName = "haveno-XMR_STAGENET_instance_" + GenUtils.getUUID();
if (!config.appName) config.appName = "haveno-" + TestConfig.baseCurrencyNetwork + "_instance_" + GenUtils.getUUID();
// connect to existing server or start new process
let havenod;
@ -1786,9 +1836,9 @@ async function initHaveno(config?: any): Promise<HavenoClient> {
// start haveno process using configured ports if available
const cmd: string[] = [
"./haveno-daemon",
"--baseCurrencyNetwork", "XMR_STAGENET",
"--useLocalhostForP2P", "true",
"--useDevPrivilegeKeys", "true",
"--baseCurrencyNetwork", TestConfig.baseCurrencyNetwork,
"--useLocalhostForP2P", TestConfig.baseCurrencyNetwork === BaseCurrencyNetwork.XMR_MAINNET ? "false" : "true", // TODO: disable for stagenet too
"--useDevPrivilegeKeys", TestConfig.baseCurrencyNetwork === BaseCurrencyNetwork.XMR_LOCAL ? "true" : "false",
"--nodePort", TestConfig.proxyPorts.get(proxyPort)![1],
"--appName", config.appName,
"--apiPassword", "apitest",
@ -1891,7 +1941,7 @@ async function initFundingWallet() {
async function startMining() {
try {
await monerod.startMining(await fundingWallet.getPrimaryAddress(), 3);
await monerod.startMining(await fundingWallet.getPrimaryAddress(), Math.max(1, Math.floor(os.cpus().length * TestConfig.maxCpuPct)));
} catch (err: any) {
if (err.message !== "Already mining") throw err;
}
@ -1953,10 +2003,14 @@ async function waitForUnlockedBalance(amount: bigint, ...wallets: any[]) {
if (!miningNeeded) return;
// wait for funds to unlock
HavenoUtils.log(0, "Mining for unlocked balance of " + amount);
HavenoUtils.log(1, "Mining for unlocked balance of " + amount);
await startMining();
const promises: Promise<void>[] = [];
for (const wallet of wallets) {
if (wallet._wallet === fundingWallet) {
const subaddress = await fundingWallet.createSubaddress(0);
HavenoUtils.log(0, "Mining to funding wallet. Alternatively, deposit to: " + subaddress.getAddress());
}
// eslint-disable-next-line no-async-promise-executor
promises.push(new Promise(async (resolve) => {
const taskLooper: any = new TaskLooper(async function() {
@ -2028,7 +2082,7 @@ async function fundOutputs(wallets: any[], amt: bigint, numOutputs?: number, wai
// collect destinations
const destinations = [];
for (const wallet of wallets) {
if (await hasUnspentOutputs([wallet], amt, numOutputs, waitForUnlock ? false : undefined)) continue;
if (await hasUnspentOutputs([wallet], amt, numOutputs, undefined)) continue;
for (let i = 0; i < numOutputs; i++) {
destinations.push(new MoneroDestination((await wallet.createSubaddress()).getAddress(), monerojs.BigInteger(amt.toString())));
}
@ -2056,8 +2110,9 @@ async function fundOutputs(wallets: any[], amt: bigint, numOutputs?: number, wai
// mine until outputs unlock
if (!waitForUnlock) return;
let miningStarted = false;
while (!await hasUnspentOutputs(wallets, amt, numOutputs, false)) {
while (!await hasUnspentOutputs(wallets, amt, numOutputs, waitForUnlock ? false : undefined)) {
if (!miningStarted) {
HavenoUtils.log(1, "Mining to fund outputs");
await startMining();
miningStarted = true;
}