automatically create and fund funding wallet

This commit is contained in:
woodser 2021-11-12 10:26:22 -05:00
parent 36a007a667
commit e66f6b4854
2 changed files with 173 additions and 95 deletions

View file

@ -8,11 +8,11 @@ This application is a lightly modified [create-react-app](https://github.com/fac
1. [Run a local Haveno test network](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md), running Alice as a daemon with `make alice-daemon`. 1. [Run a local Haveno test network](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md), running Alice as a daemon with `make alice-daemon`.
2. `git clone https://github.com/haveno-dex/haveno-ui-poc` 2. `git clone https://github.com/haveno-dex/haveno-ui-poc`
3. Start envoy with the config in ./config/envoy.yaml<br> 3. In a new terminal, start envoy with the config in haveno-ui-poc/config/envoy.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.yaml:/envoy.yaml -p 8080:8080 envoyproxy/envoy-dev:8a2143613d43d17d1eb35a24b4a4a4c432215606 -c /envoy.yaml`
Example: `docker run --rm --add-host host.docker.internal:host-gateway -it -v ~/git/haveno-ui-poc/config/envoy.yaml:/envoy.yaml -p 8080:8080 envoyproxy/envoy-dev:8a2143613d43d17d1eb35a24b4a4a4c432215606 -c /envoy.yaml` 4. `cd haveno-ui-poc`
4. `npm install` 5. `npm install`
5. `npm start` to open http://localhost:3000 in a browser 6. `npm start` to open http://localhost:3000 in a browser
6. Confirm that the Haveno daemon version is displayed (1.6.2) 7. Confirm that the Haveno daemon version is displayed (1.6.2)
<p align="center"> <p align="center">
<img src="haveno-ui-poc.png" width="500"/><br> <img src="haveno-ui-poc.png" width="500"/><br>
@ -26,12 +26,12 @@ Running the [top-level API tests](./src/HavenoDaemon.test.tsx) 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`. 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. `git clone https://github.com/haveno-dex/haveno-ui-poc` 2. `git clone https://github.com/haveno-dex/haveno-ui-poc`
3. Start envoy with the test config in ./config/envoy.test.yaml.<br> 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`
Example: `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` 4. In a new terminal, start an instance of monero-wallet-rpc at port 38084. This wallet will be automatically funded in order to fund Alice and Bob during the tests.<br>For example: `cd ~/git/haveno/.localnet/ && ./monero-wallet-rpc --daemon-address http://localhost:38081 --daemon-login superuser:abctesting123 --stagenet --rpc-bind-port 38084 --rpc-login rpc_user:abc123 --wallet-dir ./ --rpc-access-control-origins http://localhost:8080`
4. `npm install` 5. `cd haveno-ui-poc`
5. Start and fund an instance of monero-wallet-rpc at port 38084. This wallet will be used to fund the test instances of Alice and Bob.<br>For example: `cd ~/git/haveno/.localnet/ && ./monero-wallet-rpc --daemon-address http://localhost:38081 --daemon-login superuser:abctesting123 --stagenet --rpc-bind-port 38084 --rpc-login rpc_user:abc123 --wallet-dir ./ --rpc-access-control-origins http://localhost:8080` 6. `npm install`
6. Modify test config as needed in [HavenoDaemon.test.tsx](./src/HavenoDaemon.test.tsx).<br>The tests need to know the port of Alice's wallet, which is printed to Alice's console. Currently the port needs to be manually copied to the test configuration. 7. Modify test config as needed in [HavenoDaemon.test.ts](./src/HavenoDaemon.test.ts).<br>The tests need to know the port of Alice's wallet, which is printed to Alice's console. Currently the port needs to be manually copied to the test configuration.
7. `npm test` to run all tests or `npm run test -- -t 'my test'` to run tests by name. 8. `npm test` to run all tests or `npm run test -- -t 'my test'` to run tests by name.
## How to Update the Protobuf Client ## How to Update the Protobuf Client

View file

@ -1,3 +1,5 @@
// --------------------------------- IMPORTS ----------------------------------
// import haveno types // import haveno types
import {HavenoDaemon} from "./HavenoDaemon"; import {HavenoDaemon} from "./HavenoDaemon";
import {XmrBalanceInfo, OfferInfo, TradeInfo} from './protobuf/grpc_pb'; // TODO (woodser): better names; haveno_grpc_pb, haveno_pb import {XmrBalanceInfo, OfferInfo, TradeInfo} from './protobuf/grpc_pb'; // TODO (woodser): better names; haveno_grpc_pb, haveno_pb
@ -12,12 +14,22 @@ const TaskLooper = monerojs.TaskLooper;
// import console because jest swallows messages in real time // import console because jest swallows messages in real time
const console = require('console'); const console = require('console');
// --------------------------- TEST CONFIGURATION -----------------------------
// 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");
let fundingWallet: any;
// alice config // alice config
const havenoVersion = "1.6.2"; const havenoVersion = "1.6.2";
const aliceDaemonUrl = "http://localhost:8080"; const aliceDaemonUrl = "http://localhost:8080";
const aliceDaemonPassword = "apitest"; const aliceDaemonPassword = "apitest";
const alice: HavenoDaemon = new HavenoDaemon(aliceDaemonUrl, aliceDaemonPassword); const alice: HavenoDaemon = new HavenoDaemon(aliceDaemonUrl, aliceDaemonPassword);
const aliceWalletUrl = "http://127.0.0.1:63773"; // alice's internal haveno wallet for direct testing // TODO (woodser): make configurable rather than randomly generated const aliceWalletUrl = "http://127.0.0.1:64840"; // alice's internal haveno wallet for direct testing // TODO (woodser): make configurable rather than randomly generated
const aliceWalletUsername = "rpc_user"; const aliceWalletUsername = "rpc_user";
const aliceWalletPassword = "abc123"; const aliceWalletPassword = "abc123";
let aliceWallet: any; let aliceWallet: any;
@ -33,12 +45,6 @@ const moneroDaemonUsername = "superuser";
const moneroDaemonPassword = "abctesting123"; const moneroDaemonPassword = "abctesting123";
let monerod: any; let monerod: any;
// source funding wallet
const fundingWalletUrl = "http://localhost:38084";
const fundingWalletUsername = "rpc_user";
const fundingWalletPassword = "abc123";
let fundingWallet: any;
// other test config // other test config
const WALLET_SYNC_PERIOD = 5000; const WALLET_SYNC_PERIOD = 5000;
const MAX_TIME_PEER_NOTICE = 3000; const MAX_TIME_PEER_NOTICE = 3000;
@ -53,11 +59,17 @@ const TEST_CRYPTO_ACCOUNTS = [ // TODO (woodser): test other cryptos, fiat
} }
]; ];
// ----------------------------------- TESTS ----------------------------------
beforeAll(async () => { beforeAll(async () => {
// initialize clients of daemon and wallet rpc // initialize client of monerod
monerod = await monerojs.connectToDaemonRpc(moneroDaemonUrl, moneroDaemonUsername, moneroDaemonPassword); monerod = await monerojs.connectToDaemonRpc(moneroDaemonUrl, moneroDaemonUsername, moneroDaemonPassword);
fundingWallet = await monerojs.connectToWalletRpc(fundingWalletUrl, fundingWalletUsername, fundingWalletPassword);
// initialize funding wallet
await initFundingWallet();
// create client connected to alice's internal wallet
aliceWallet = await monerojs.connectToWalletRpc(aliceWalletUrl, aliceWalletUsername, aliceWalletPassword); aliceWallet = await monerojs.connectToWalletRpc(aliceWalletUrl, aliceWalletUsername, aliceWalletPassword);
await aliceWallet.startSyncing(WALLET_SYNC_PERIOD); await aliceWallet.startSyncing(WALLET_SYNC_PERIOD);
@ -69,6 +81,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);
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(havenoVersion); expect(version).toEqual(havenoVersion);
@ -162,9 +175,9 @@ test("Can create crypto payment accounts", async () => {
test("Can post and remove an offer", async () => { test("Can post and remove an offer", async () => {
// wait for alice and bob to have unlocked balance for trade // wait for alice to have unlocked balance to post offer
let tradeAmount: bigint = BigInt("250000000000"); let tradeAmount: bigint = BigInt("250000000000");
await waitForUnlockedBalance(tradeAmount, alice, bob); await waitForUnlockedBalance(tradeAmount, alice);
// get unlocked balance before reserving funds for offer // get unlocked balance before reserving funds for offer
let unlockedBalanceBefore: bigint = BigInt((await alice.getBalances()).getUnlockedBalance()); let unlockedBalanceBefore: bigint = BigInt((await alice.getBalances()).getUnlockedBalance());
@ -182,7 +195,6 @@ test("Can post and remove an offer", async () => {
expect(unlockedBalanceBefore).toEqual(BigInt((await alice.getBalances()).getUnlockedBalance())); expect(unlockedBalanceBefore).toEqual(BigInt((await alice.getBalances()).getUnlockedBalance()));
}); });
jest.setTimeout(15000);
test("Invalidates offers when reserved funds are spent", async () => { test("Invalidates offers when reserved funds are spent", async () => {
// wait for alice and bob to have unlocked balance for trade // wait for alice and bob to have unlocked balance for trade
@ -236,7 +248,6 @@ test("Invalidates offers when reserved funds are spent", async () => {
await monerod.flushTxPool(tx.getHash()); await monerod.flushTxPool(tx.getHash());
}); });
jest.setTimeout(120000);
test("Can complete a trade", async () => { test("Can complete a trade", async () => {
// wait for alice and bob to have unlocked balance for trade // wait for alice and bob to have unlocked balance for trade
@ -311,6 +322,144 @@ test("Can complete a trade", async () => {
// ------------------------------- HELPERS ------------------------------------ // ------------------------------- HELPERS ------------------------------------
/**
* Open or create funding wallet.
*/
async function initFundingWallet() {
// init client connected to monero-wallet-rpc
fundingWallet = await monerojs.connectToWalletRpc(fundingWalletUrl, fundingWalletUsername, fundingWalletPassword);
// check if wallet is open
let walletIsOpen = false
try {
await fundingWallet.getPrimaryAddress();
walletIsOpen = true;
} catch (err) { }
// open wallet if necessary
if (!walletIsOpen) {
// attempt to open funding wallet
try {
await fundingWallet.openWallet({path: defaultFundingWalletPath, password: fundingWalletPassword});
} catch (e) {
if (!(e instanceof monerojs.MoneroRpcError)) throw e;
// -1 returned when wallet does not exist or fails to open e.g. it's already open by another application
if (e.getCode() === -1) {
// create wallet
await fundingWallet.createWallet({path: defaultFundingWalletPath, password: fundingWalletPassword});
} else {
throw e;
}
}
}
}
/**
* Wait for unlocked balance in wallet or Haveno daemon.
*/
async function waitForUnlockedBalance(amount: bigint, ...wallets: any[]) {
// wrap common wallet functionality for tests
class WalletWrapper {
_wallet: any;
constructor(wallet: any) {
this._wallet = wallet;
}
async getUnlockedBalance(): Promise<bigint> {
if (this._wallet instanceof HavenoDaemon) return BigInt((await this._wallet.getBalances()).getUnlockedBalance());
else return BigInt((await this._wallet.getUnlockedBalance()).toString());
}
async getLockedBalance(): Promise<bigint> {
if (this._wallet instanceof HavenoDaemon) return BigInt((await this._wallet.getBalances()).getLockedBalance());
else return BigInt((await this._wallet.getBalance()).toString()) - await this.getUnlockedBalance();
}
async getDepositAddress(): Promise<string> {
if (this._wallet instanceof HavenoDaemon) return await this._wallet.getNewDepositSubaddress();
else return await this._wallet.getPrimaryAddress();
}
}
// wrap wallets
for (let i = 0; i < wallets.length; i++) wallets[i] = new WalletWrapper(wallets[i]);
// fund wallets with insufficient balance
let miningNeeded = false;
let fundConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(true);
for (let wallet of wallets) {
let unlockedBalance = await wallet.getUnlockedBalance();
if (unlockedBalance < amount) miningNeeded = true;
let depositNeeded: bigint = amount - unlockedBalance - await wallet.getLockedBalance();
if (depositNeeded > BigInt("0") && wallet._wallet !== fundingWallet) fundConfig.addDestination(await wallet.getDepositAddress(), depositNeeded);
}
if (fundConfig.getDestinations()) {
await waitForUnlockedBalance(minimumFunding, 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); }
}
// done if all wallets have sufficient unlocked balance
if (!miningNeeded) return;
// wait for funds to unlock
console.log("Mining for unlocked balance of " + amount);
await startMining();
let promises: Promise<void>[] = []
for (let wallet of wallets) {
promises.push(new Promise(async function(resolve, reject) {
let taskLooper: any = new TaskLooper(async function() {
if (await wallet.getUnlockedBalance() >= amount) {
taskLooper.stop();
resolve();
}
});
taskLooper.start(5000);
}));
}
await Promise.all(promises);
await monerod.stopMining();
console.log("Funds unlocked, done mining");
};
async function waitForUnlockedTxs(...txHashes: string[]) {
await startMining();
let promises: Promise<void>[] = []
for (let txHash of txHashes) {
promises.push(new Promise(async function(resolve, reject) {
let taskLooper: any = new TaskLooper(async function() {
let tx = await monerod.getTx(txHash);
if (tx.isConfirmed() && tx.getBlock().getHeight() <= await monerod.getHeight() - 10) {
taskLooper.stop();
resolve();
}
});
taskLooper.start(5000);
}));
}
await Promise.all(promises);
await monerod.stopMining();
}
async function startMining() {
try {
await monerod.startMining(await fundingWallet.getPrimaryAddress(), 1);
} catch (err) {
if (err.message !== "Already mining") throw err;
}
}
async function wait(durationMs: number) {
return new Promise(function(resolve) { setTimeout(resolve, durationMs); });
}
async function postOffer() { // TODO (woodser): postOffer(maker, peer) async function postOffer() { // TODO (woodser): postOffer(maker, peer)
// test requires ethereum payment account // test requires ethereum payment account
@ -382,74 +531,3 @@ function testOffer(offer: OfferInfo) {
expect(offer.getId().length).toBeGreaterThan(0); expect(offer.getId().length).toBeGreaterThan(0);
// TODO: test rest of offer // TODO: test rest of offer
} }
async function wait(durationMs: number) {
return new Promise(function(resolve) { setTimeout(resolve, durationMs); });
}
async function startMining() {
try {
await monerod.startMining(await fundingWallet.getPrimaryAddress(), 1);
} catch (err) {
if (err.message !== "Already mining") throw err;
}
}
async function waitForUnlockedBalance(amount: bigint, ...clients: HavenoDaemon[]) {
// fund haveno clients with insufficient balance
let miningNeeded = false;
let fundConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(true);
for (let client of clients) {
let balances = await client.getBalances();
if (BigInt(balances.getUnlockedBalance()) < amount) miningNeeded = true;
let depositNeeded: BigInt = amount - BigInt(balances.getUnlockedBalance()) - BigInt(balances.getLockedBalance());
if (depositNeeded > BigInt("0")) fundConfig.addDestination(await client.getNewDepositSubaddress(), depositNeeded);
}
if (fundConfig.getDestinations()) {
try { await fundingWallet.createTx(fundConfig); }
catch (err) { throw new Error("Error funding haveno daemons: " + err.message); }
}
// done if all clients have sufficient unlocked balance
if (!miningNeeded) return;
// wait for funds to unlock
console.log("Mining for unlocked trader balances of " + amount);
await startMining();
let promises: Promise<void>[] = []
for (let client of clients) {
promises.push(new Promise(async function(resolve, reject) {
let taskLooper: any = new TaskLooper(async function() {
let balances: XmrBalanceInfo = await client.getBalances();
if (BigInt(balances.getUnlockedBalance()) >= amount) {
taskLooper.stop();
resolve();
}
});
taskLooper.start(5000);
}));
}
await Promise.all(promises);
await monerod.stopMining();
console.log("Funds unlocked, done mining");
};
async function waitForUnlockedTxs(...txHashes: string[]) {
await startMining();
let promises: Promise<void>[] = []
for (let txHash of txHashes) {
promises.push(new Promise(async function(resolve, reject) {
let taskLooper: any = new TaskLooper(async function() {
let tx = await monerod.getTx(txHash);
if (tx.isConfirmed() && tx.getBlock().getHeight() <= await monerod.getHeight() - 10) {
taskLooper.stop();
resolve();
}
});
taskLooper.start(5000);
}));
}
await Promise.all(promises);
await monerod.stopMining();
}