convert to typescript library

remove create-react-app
This commit is contained in:
woodser 2022-05-04 18:14:28 -04:00
parent 1281f9223f
commit 7ee0d34f1a
28 changed files with 9917 additions and 22826 deletions

View File

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

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ backup.zip
# production
/build
/dist
# misc
.DS_Store

View File

@ -1,33 +1,35 @@
# Haveno UI Proof of Concept
# Haveno TypeScript Library
A proof of concept to fetch and render data from Haveno's daemon in ReactJS.
TypeScript library for using Haveno.
This application is a lightly modified [create-react-app](https://github.com/facebook/create-react-app) with typescript using [envoy proxy](https://www.envoyproxy.io/) and [grpc-web](https://github.com/grpc/grpc-web) to use Haveno's gRPC API.
## Sample code
## Run in a Browser
```js
import { HavenoClient } from "haveno-ts";
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. 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.yaml (change absolute path for your system): `docker run --rm --add-host host.docker.internal:host-gateway -it -v ~/git/haveno-ts/config/envoy.yaml:/envoy.yaml -p 8080:8080 envoyproxy/envoy-dev:8a2143613d43d17d1eb35a24b4a4a4c432215606 -c /envoy.yaml`
4. Install protobuf compiler v3.19.1 or later for your system:<br>
mac: `brew install protobuf`<br>
linux: `apt install protobuf-compiler`
NOTE: You may need to upgrade to v3.19.1 manually if your package manager installs an older version.
5. Download `protoc-gen-grpc-web` plugin and make executable as [shown here](https://github.com/grpc/grpc-web#code-generator-plugin).
6. `cd haveno-ts`
7. `npm install`
8. `npm start` to open http://localhost:3000 in a browser
9. Confirm that the Haveno daemon version is displayed (1.6.2).
// create client connected to Haveno daemon
const alice = new HavenoClient("http://localhost:8080", "apitest");
<p align="center">
<img src="haveno-ui-poc.png" width="500"/><br>
</p>
// use Haveno daemon
const balances = await alice.getBalances();
const paymentAccounts = await alice.getPaymentAccounts();
const myOffers = await alice.getMyOffers("ETH");
const offers = await alice.getOffers("ETH", "BUY");
const trade = await alice.takeOffer(offers[0].getId(), paymentAccounts[0].getId());
## Run Tests
// disconnect client
await alice.disconnect();
```
Running the [API tests](./src/haveno.test.ts) is the best way to develop and test Haveno end-to-end.
## TypeDocs
[`haveno.ts`](./src/haveno.ts) provides the interface to Haveno's backend daemon.
See haveno-ts [typedocs](https://haveno-dex.github.io/haveno-ts/classes/haveno.HavenoClient.html).
## Run tests
Running the [API tests](./src/HavenoClient.test.ts) is the best way to develop and test Haveno end-to-end.
[`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.
2. Clone this project to the same parent directory as the haveno project: `git clone https://github.com/haveno-dex/haveno-ts`

6
babel.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

32289
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,58 +1,57 @@
{
"name": "haveno-ts",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^12.8.3",
"@types/jest": "^26.0.22",
"@types/node": "^17.0.30",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.3",
"console": "^0.7.2",
"google-protobuf": "^3.0.0",
"grpc-web": "^1.2.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "^4.0.3",
"typescript": "^4.6.3",
"web-vitals": "^1.1.1"
},
"version": "0.0.1",
"description": "Haveno TypeScript interface",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist/**/*"],
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --runInBand",
"eject": "react-scripts eject",
"prepare": "bin/build_protobuf.sh",
"pretest": "bin/build_protobuf.sh",
"prepare": "scripts/build_protobuf.sh",
"pretest": "scripts/build_protobuf.sh",
"build": "tsc",
"test": "jest ",
"eslint": "eslint .",
"eslintfix": "eslint src/* --fix",
"typedoc": "typedoc ./src/haveno.ts --entryPointStrategy expand src/ --exclude **/*.test.ts"
"typedoc": "typedoc ./src/index.ts --entryPointStrategy expand src/ --exclude **/*.test.ts"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"jest": {
"testPathIgnorePatterns": ["<rootDir>/dist/", "/node_modules/"]
},
"repository": {
"type": "git",
"url": "git+https://github.com/haveno-dex/haveno-ts.git"
},
"keywords": [],
"author": "",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/haveno-dex/haveno-ts/issues"
},
"homepage": "https://github.com/haveno-dex/haveno-ts#readme",
"dependencies": {
"@types/node": "^17.0.30",
"console": "^0.7.2",
"google-protobuf": "^3.0.0",
"grpc-web": "^1.2.1"
},
"devDependencies": {
"@babel/core": "^7.17.10",
"@babel/preset-env": "^7.17.10",
"@babel/preset-typescript": "^7.16.7",
"@types/jest": "^27.4.1",
"@typescript-eslint/eslint-plugin": "5.12.1",
"@typescript-eslint/parser": "^5.19.0",
"babel-jest": "^28.0.3",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^2.7.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"jest": "^26.6.0",
"monero-javascript": "^0.6.4",
"typedoc": "^0.22.15",
"typedoc-plugin-missing-exports": "^0.22.6",
"typedoc-plugin-rename-defaults": "^0.5.1"
"typedoc-plugin-rename-defaults": "^0.5.1",
"typescript": "^4.6.4"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Haveno DEX</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -1,25 +0,0 @@
{
"short_name": "Haveno DEX",
"name": "Haveno Decentralized Exchange",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,23 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
.App-header {
background-color: rgb(38, 26, 43);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}

View File

@ -1,9 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn more/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -1,53 +0,0 @@
import React from 'react';
import logo from './logo.png';
import './App.css';
import { HavenoClient } from './haveno';
const HAVENO_DAEMON_URL = "http://localhost:8080";
const HAVENO_DAEMON_PASSWORD = "apitest";
class App extends React.Component<{}, {daemonVersion: string}> {
havenod: HavenoClient;
constructor(props: any) {
super(props);
this.state = {daemonVersion: ""};
this.havenod = new HavenoClient(HAVENO_DAEMON_URL, HAVENO_DAEMON_PASSWORD);
}
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Version {this.state.daemonVersion}
</p>
<p>
Coming soon...
</p>
<a
className="App-link"
href="https://haveno.exchange"
target="_blank"
rel="noopener noreferrer"
>
Learn More
</a>
</header>
</div>
);
}
async componentDidMount() {
try {
this.setState({daemonVersion: await this.havenod.getVersion()});
} catch (err) {
console.error(err);
this.setState({daemonVersion: " not available"});
}
}
}
export default App;

View File

@ -1,7 +1,7 @@
// --------------------------------- IMPORTS ----------------------------------
// import haveno types
import { HavenoClient } from "./haveno";
import HavenoClient from "./HavenoClient";
import HavenoUtils from "./utils/HavenoUtils";
import * as grpcWeb from "grpc-web";
import { MarketPriceInfo, NotificationMessage, OfferInfo, TradeInfo, UrlConnection, XmrBalanceInfo } from "./protobuf/grpc_pb"; // TODO (woodser): better names; haveno_grpc_pb, haveno_pb
@ -202,15 +202,22 @@ beforeAll(async () => {
// initialize funding wallet
await initFundingWallet();
// create test data directory if it doesn't exist
if (!fs.existsSync(TestConfig.testDataDir)) fs.mkdirSync(TestConfig.testDataDir);
});
beforeEach(async() => {
beforeEach(async () => {
HavenoUtils.log(0, "BEFORE TEST \"" + expect.getState().currentTestName + "\"");
});
afterAll(async () => {
monerod.worker.terminate(); // TODO (woodser): support terminating daemon and full wallet worker, e.g. daemon.disconnect()
const promises = [];
for (const havenod of startupHavenods) if (havenod.getProcess()) promises.push(releaseHavenoProcess(havenod));
for (const havenod of startupHavenods) {
if (havenod.getProcess()) promises.push(releaseHavenoProcess(havenod));
else promises.push(havenod.disconnect());
}
return Promise.all(promises);
});

View File

@ -9,7 +9,7 @@ import { PaymentMethod, PaymentAccount, AvailabilityResult, Attachment, DisputeR
/**
* Haveno daemon client using gRPC.
*/
class HavenoClient {
export default class HavenoClient {
// grpc clients
_appName: string | undefined;
@ -17,6 +17,7 @@ class HavenoClient {
_disputeAgentsClient: DisputeAgentsClient;
_disputesClient: DisputesClient;
_notificationsClient: NotificationsClient;
_notificationStream: grpcWeb.ClientReadableStream<NotificationMessage> | undefined;
_moneroConnectionsClient: MoneroConnectionsClient;
_moneroNodeClient: MoneroNodeClient;
_walletsClient: WalletsClient;
@ -33,7 +34,7 @@ class HavenoClient {
_process: any;
_processLogging = false;
_walletRpcPort: number | undefined;
_notificationListeners: ((notification: NotificationMessage) => void)[] = [];
_notificationListeners: ((_notification: NotificationMessage) => void)[] = [];
_registerNotificationListenerCalled = false;
_keepAliveLooper: any;
_keepAlivePeriodMs = 60000;
@ -386,9 +387,9 @@ class HavenoClient {
*
* @param {(notification: NotificationMessage) => void} listener - the notification listener to add
*/
async addNotificationListener(listener: (notification: NotificationMessage) => void): Promise<void> {
async addNotificationListener(listener: (_notification: NotificationMessage) => void): Promise<void> {
this._notificationListeners.push(listener);
return this._registerNotificationListenerOnce();
return this._updateNotificationListenerRegistration();
}
/**
@ -396,10 +397,11 @@ class HavenoClient {
*
* @param {(notification: NotificationMessage) => void} listener - the notification listener to remove
*/
async removeNotificationListener(listener: (notification: NotificationMessage) => void): Promise<void> {
async removeNotificationListener(listener: (_notification: NotificationMessage) => void): Promise<void> {
const idx = this._notificationListeners.indexOf(listener);
if (idx > -1) this._notificationListeners.splice(idx, 1);
else throw new Error("Notification listener is not registered");
return this._updateNotificationListenerRegistration();
}
/**
@ -1168,11 +1170,18 @@ class HavenoClient {
});
}
/**
* Disconnect this client from the server.
*/
async disconnect() {
while (this._notificationListeners.length) await this.removeNotificationListener(this._notificationListeners[0]);
}
/**
* Shutdown the Haveno daemon server and stop the process if applicable.
*/
async shutdownServer() {
if (this._keepAliveLooper) this._keepAliveLooper.stop();
await this.disconnect();
await new Promise<void>((resolve, reject) => {
this._shutdownServerClient.stop(new StopRequest(), {password: this._password}, function(err: grpcWeb.RpcError) { // process receives 'exit' event
if (err) reject(err);
@ -1227,43 +1236,46 @@ class HavenoClient {
});
});
}
/**
* Register a listener to receive notifications.
* Update notification listener registration.
* Due to the nature of grpc streaming, this method returns a promise
* which may be resolved before the listener is actually registered.
*
* @hidden
*/
async _registerNotificationListenerOnce(): Promise<void> {
if (this._registerNotificationListenerCalled) return;
else this._registerNotificationListenerCalled = true;
return new Promise((resolve) => {
async _updateNotificationListenerRegistration(): Promise<void> {
const listening = this._notificationListeners.length > 0;
if (listening && this._notificationStream || !listening && !this._notificationStream) return; // no difference
if (listening) {
return new Promise((resolve) => {
// send request to register client listener
this._notificationsClient.registerNotificationListener(new RegisterNotificationListenerRequest(), {password: this._password})
.on('data', (data) => {
if (data instanceof NotificationMessage) {
for (const listener of this._notificationListeners) listener(data);
// send request to register client listener
this._notificationStream = this._notificationsClient.registerNotificationListener(new RegisterNotificationListenerRequest(), {password: this._password})
.on('data', (data) => {
if (data instanceof NotificationMessage) {
for (const listener of this._notificationListeners) listener(data);
}
});
// periodically send keep alive requests // TODO (woodser): better way to keep notification stream alive?
let firstRequest = true;
this._keepAliveLooper = new TaskLooper(async () => {
if (firstRequest) {
firstRequest = false;
return;
}
await this._sendNotification(new NotificationMessage()
.setType(NotificationMessage.NotificationType.KEEP_ALIVE)
.setTimestamp(Date.now()));
});
// periodically send keep alive requests // TODO (woodser): better way to keep notification stream alive?
let firstRequest = true;
this._keepAliveLooper = new TaskLooper(async () => {
if (firstRequest) {
firstRequest = false;
return;
}
await this._sendNotification(new NotificationMessage()
.setType(NotificationMessage.NotificationType.KEEP_ALIVE)
.setTimestamp(Date.now()));
this._keepAliveLooper.start(this._keepAlivePeriodMs);
setTimeout(resolve, 1000); // TODO: call returns before listener registered
});
this._keepAliveLooper.start(this._keepAlivePeriodMs);
setTimeout(resolve, 1000); // TODO: call returns before listener registered
});
} else {
this._keepAliveLooper.stop();
this._notificationStream!.cancel();
this._notificationStream = undefined;
}
}
/**
@ -1300,5 +1312,3 @@ class HavenoClient {
});
}
}
export { HavenoClient };

View File

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

4
src/index.ts Normal file
View File

@ -0,0 +1,4 @@
import HavenoClient from "./HavenoClient";
import HavenoUtils from "./utils/HavenoUtils";
export {HavenoClient, HavenoUtils}

View File

@ -1,17 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@ -1,15 +0,0 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@ -6,6 +6,7 @@ export default class TaskLooper {
_fn: () => Promise<void>;
_isStarted: boolean;
_isLooping: boolean;
_timeout: NodeJS.Timeout | undefined;
/**
* Build the looper with a function to invoke on a fixed period loop.
@ -33,7 +34,10 @@ export default class TaskLooper {
* Stop the task loop.
*/
stop() {
if (!this._isStarted) throw new Error("Cannot stop TaskLooper because it's not started");
this._isStarted = false;
clearTimeout(this._timeout!);
this._timeout = undefined;
}
async _runLoop(periodInMs: number) {
@ -41,7 +45,7 @@ export default class TaskLooper {
while (this._isStarted) {
const startTime = Date.now();
await this._fn();
if (this._isStarted) await new Promise(function(resolve) { setTimeout(resolve, periodInMs - (Date.now() - startTime)); });
if (this._isStarted) await new Promise((resolve) => { this._timeout = setTimeout(resolve, periodInMs - (Date.now() - startTime)); });
}
this._isLooping = false;
}

View File

@ -1,11 +1,14 @@
{
"compilerOptions": {
"outDir": "./dist",
"target": "es2021",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"declaration": true,
"sourceMap": true,
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@ -17,10 +20,8 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
"include": ["src"],
"exclude": ["node_modules", "**/*.test.ts"]
}