Implement peer package to send messages between peers (#279)

* Initial implementation of class and discoving other peers

* Implement peer connection and sending messages between peers

* Add react app and use peer package for broadcasting

* Maintain stream for each remote peer

* Refactor code in peer package

* Add serve package for react-app

* Add readme for running react app

* Add peer package readme

* Add logs for events with details

* Add a chat CLI using peer package (#280)

* Add a flag to instantiate Peer for nodejs

* Add a basic chat CLI using peer

* Add a signal server arg to chat CLI

* Add instructions for chat CLI

* Fix typescript and ESM issues after adding peer package (#282)

* Fix build issues in util package

* Update eslint TS plugins

* Scope react app package name

* Convert cli package back to CJS and dynamically import ESM

* Upgrade ts-node version

* Fix tests

* Setup a relay node and pubsub based discovery (#284)

* Add a script to setup a relay node

* Use pubsub based peer discovery

* Add peer multiaddr to connection log

* Catch relay node dial errors

* Increase discovery interval and dial all mutiaddr

* Add UI to display self peer ID and multiaddrs

* Add UI for displaying live remote connections

* Send js objects in peer broadcast messages

* Add react-peer package for using peer in react app

* Reduce disconnect frequency between peers (#287)

* Restrict number of max concurrent dials per peer

* Increase hop relay timeout to 1 day

* Self review changes

* Review changes

* Increase pubsub discovery interval and add steps to create a peer id  (#290)

* Increase pubsub discovery interval

* Disable autodial to avoid peer dials without protocol on a reconnect

* Add steps to create a peer id and use for relay node

* Add back dependency to run signalling server

* Avoid bootstrapping and dial to relay node directly

Co-authored-by: prathamesh0 <42446521+prathamesh0@users.noreply.github.com>
Co-authored-by: prathamesh0 <prathamesh.musale0@gmail.com>
This commit is contained in:
Nabarun Gogoi 2023-01-10 20:10:27 +05:30 committed by GitHub
parent aa4a954330
commit cd29b47ecc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 17136 additions and 2475 deletions

2
.npmrc
View File

@ -1 +1 @@
registry=https://git.vdb.to/api/packages/cerc-io/npm/
@cerc-io:registry=https://git.vdb.to/api/packages/cerc-io/npm/

View File

@ -48,8 +48,8 @@
"@types/express": "^4.17.14",
"@types/mocha": "^8.2.2",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"chai": "^4.3.4",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",

View File

@ -29,8 +29,8 @@
"@types/debug": "^4.1.5",
"@types/fs-extra": "^9.0.11",
"@types/level": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",

View File

@ -69,7 +69,7 @@ export class Cache {
log(`${this._name}: cache hit ${key}`);
return [value, true];
} catch (err) {
} catch (err: any) {
log(`${this._name}: cache miss ${key}`);
if (err.notFound) {

56
packages/cli/README.md Normal file
View File

@ -0,0 +1,56 @@
# cli
## chat
A basic CLI to pass messages between peers using `stdin`/`stdout`
* Install dependencies:
```bash
yarn install
```
* Build the `peer` package:
```
cd packages/peer
yarn build
```
* (Optional) Run a local signalling server:
```bash
# In packages/peer
yarn signal-server
```
* (Optional) Create and export a peer id for the relay node:
```bash
# In packages/peer
yarn create-peer --file [PEER_ID_FILE_PATH]
```
* `file (f)`: file path to export the peer id to (json)
* (Optional) Run a local relay node:
```bash
# In packages/peer
yarn relay-node --signal-server [SIGNAL_SERVER_URL] --peer-id-file [PEER_ID_FILE_PATH]
```
* `signal-server`: multiaddr of a signalling server (default: local signalling server multiaddr)
* `peer-id-file`: file path for peer id to be used (json)
* Start the node:
```bash
# In packages/cli
yarn chat --signal-server [SIGNAL_SERVER_URL] --relay-node [RELAY_NODE_URL]
```
* `signal-server`: multiaddr of a signalling server (default: local signalling server multiaddr)
* `relay-node`: multiaddr of a hop enabled relay node
* The process starts reading from `stdin` and outputs messages from others peers to `stdout`.

View File

@ -7,13 +7,16 @@
"lint": "eslint .",
"build": "yarn clean && tsc && yarn copy-assets",
"clean": "rm -rf ./dist",
"copy-assets": "copyfiles -u 1 src/**/*.gql dist/"
"copy-assets": "copyfiles -u 1 src/**/*.gql dist/",
"chat": "node dist/chat.js"
},
"dependencies": {
"@cerc-io/peer": "^0.2.18",
"@cerc-io/util": "^0.2.18",
"@ethersproject/providers": "^5.4.4",
"@graphql-tools/utils": "^9.1.1",
"@ipld/dag-cbor": "^6.0.12",
"@ipld/dag-cbor": "^8.0.0",
"@libp2p/interface-peer-id": "^1.1.2",
"apollo-server-express": "^3.11.1",
"debug": "^4.3.1",
"express": "^4.18.2",
@ -24,13 +27,15 @@
},
"devDependencies": {
"@types/express": "^4.17.14",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@types/node": "16.11.7",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^5.0.0",
"eslint-plugin-import": "^2.23.3",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0"
"eslint-plugin-standard": "^5.0.0",
"typescript": "^4.9.4"
}
}

63
packages/cli/src/chat.ts Normal file
View File

@ -0,0 +1,63 @@
//
// Copyright 2022 Vulcanize, Inc.
//
import * as readline from 'readline';
import { hideBin } from 'yargs/helpers';
import yargs from 'yargs';
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1319854183
import { PeerId } from '@libp2p/interface-peer-id';
interface Arguments {
signalServer: string;
relayNode: string;
}
async function main (): Promise<void> {
const argv: Arguments = _getArgv();
if (!argv.signalServer) {
console.log('Using the default signalling server URL');
}
// https://adamcoster.com/blog/commonjs-and-esm-importexport-compatibility-examples#importing-esm-into-commonjs-cjs
const { Peer } = await import('@cerc-io/peer');
const peer = new Peer(true);
await peer.init(argv.signalServer, argv.relayNode);
peer.subscribeMessage((peerId: PeerId, message: string) => {
console.log(`> ${peerId.toString()} > ${message}`);
});
console.log(`Peer ID: ${peer.peerId?.toString()}`);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.on('line', (input: string) => {
peer.broadcastMessage(input);
});
console.log('Reading input...');
}
function _getArgv (): any {
return yargs(hideBin(process.argv)).parserConfiguration({
'parse-numbers': false
}).options({
signalServer: {
type: 'string',
describe: 'Signalling server URL'
},
relayNode: {
type: 'string',
describe: 'Relay node URL'
}
}).argv;
}
main().catch(err => {
console.log(err);
});

View File

@ -22,7 +22,6 @@ import {
GraphWatcherInterface,
Config
} from '@cerc-io/util';
import * as codec from '@ipld/dag-cbor';
import { BaseCmd } from './base';
@ -164,6 +163,7 @@ export class ExportStateCmd {
}
if (this._argv.exportFile) {
const codec = await import('@ipld/dag-cbor');
const encodedExportData = codec.encode(exportData);
const filePath = path.resolve(this._argv.exportFile);

View File

@ -25,7 +25,6 @@ import {
updateEntitiesFromState,
Config
} from '@cerc-io/util';
import * as codec from '@ipld/dag-cbor';
import { BaseCmd } from './base';
@ -112,6 +111,7 @@ export class ImportStateCmd {
// Import data.
const importFilePath = path.resolve(this._argv.importFile);
const encodedImportData = fs.readFileSync(importFilePath);
const codec = await import('@ipld/dag-cbor');
const importData = codec.decode(Buffer.from(encodedImportData)) as any;
// Fill the snapshot block.

View File

@ -5,7 +5,7 @@
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"module": "CommonJS", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": [ "ES5", "ES6", "ES2020" ], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
@ -44,13 +44,11 @@
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"moduleResolution": "Node16", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": [
"./src/types"
], /* List of folders to include type definitions from. */
"typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */

View File

@ -41,8 +41,8 @@
"@openzeppelin/contracts": "^4.3.2",
"@types/js-yaml": "^4.0.3",
"@types/node": "^16.9.0",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",

View File

@ -4,7 +4,7 @@
import assert from 'assert';
import debug from 'debug';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import { DeepPartial, FindConditions, FindManyOptions, ObjectLiteral } from 'typeorm';
import JSONbig from 'json-bigint';
import { ethers } from 'ethers';
{{#if (subgraphPath)}}
@ -368,7 +368,7 @@ export class Indexer implements IndexerInterface {
}
{{#if (subgraphPath)}}
async getSubgraphEntity<Entity> (
async getSubgraphEntity<Entity extends ObjectLiteral> (
entity: new () => Entity,
id: string,
block: BlockHeight,
@ -379,7 +379,7 @@ export class Indexer implements IndexerInterface {
return data;
}
async getSubgraphEntities<Entity> (
async getSubgraphEntities<Entity extends ObjectLiteral> (
entity: new () => Entity,
block: BlockHeight,
where: { [key: string]: any } = {},

View File

@ -61,8 +61,8 @@
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
@ -70,7 +70,7 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0",
"ts-node": "^10.0.0",
"ts-node": "^10.2.1",
"typescript": "^4.3.2",
"copyfiles": "^2.4.1"
}

View File

@ -56,8 +56,8 @@
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",
@ -66,7 +66,7 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0",
"ts-node": "^10.0.0",
"ts-node": "^10.2.1",
"typescript": "^4.3.2"
}
}

View File

@ -3,7 +3,7 @@
//
import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import { DeepPartial, FindConditions, FindManyOptions, ObjectLiteral } from 'typeorm';
import { ethers } from 'ethers';
import { SelectionNode } from 'graphql';
@ -281,13 +281,13 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.removeStates(blockNumber, kind);
}
async getSubgraphEntity<Entity> (entity: new () => Entity, id: string, block: BlockHeight, selections: ReadonlyArray<SelectionNode> = []): Promise<any> {
async getSubgraphEntity<Entity extends ObjectLiteral> (entity: new () => Entity, id: string, block: BlockHeight, selections: ReadonlyArray<SelectionNode> = []): Promise<any> {
const data = await this._graphWatcher.getEntity(entity, id, this._relationsMap, block, selections);
return data;
}
async getSubgraphEntities<Entity> (
async getSubgraphEntities<Entity extends ObjectLiteral> (
entity: new () => Entity,
block: BlockHeight,
where: { [key: string]: any } = {},

View File

@ -2,7 +2,7 @@
host = "127.0.0.1"
port = 3001
mode = "eth_call"
kind = "lazy"
kind = "active"
[metrics]
host = "127.0.0.1"

View File

@ -62,8 +62,8 @@
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@openzeppelin/contracts": "^4.3.2",
"@types/json-bigint": "^1.0.0",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
@ -72,6 +72,7 @@
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0",
"hardhat": "^2.3.0",
"nodemon": "^2.0.7"
"nodemon": "^2.0.7",
"ts-node": "^10.2.1"
}
}

View File

@ -54,7 +54,7 @@ interface EventResult {
to?: string;
owner?: string;
spender?: string;
value?: BigInt;
value?: bigint;
__typename: string;
}
proof?: string;

View File

@ -68,8 +68,8 @@
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@openzeppelin/contracts": "^4.3.2",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",
@ -79,7 +79,7 @@
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0",
"hardhat": "^2.3.0",
"ts-node": "^10.0.0",
"ts-node": "^10.2.1",
"typescript": "^4.3.2"
}
}

View File

@ -12,8 +12,8 @@
"@types/chai": "^4.2.18",
"@types/chai-spies": "^1.0.3",
"@types/pluralize": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"chai": "^4.3.4",
"chai-spies": "^1.0.0",
"eslint": "^7.27.0",
@ -27,7 +27,7 @@
"hardhat": "^2.3.0",
"mocha": "^8.4.0",
"nodemon": "^2.0.7",
"ts-node": "^10.0.0",
"ts-node": "^10.2.1",
"typescript": "^4.3.2"
},
"bin": {

View File

@ -102,7 +102,7 @@ describe('wasm loader tests', () => {
}
expect.fail('wasm code should throw error');
} catch (error) {
} catch (error: any) {
expect(error).to.be.instanceof(WebAssembly.RuntimeError);
expect(error.message).to.equal('unreachable');
}

View File

@ -576,7 +576,7 @@ describe('numbers wasm tests', () => {
it('should throw an error for DECIMAL128_PMIN times DECIMAL128_NMAX', async () => {
try {
await testBigDecimalTimes(await __newString(DECIMAL128_PMIN), await __newString(DECIMAL128_NMAX));
} catch (error) {
} catch (error: any) {
expect(error.message).to.be.equal('Big decimal exponent \'-12286\' is outside the \'-6143\' to \'6144\' range');
}
});
@ -621,7 +621,7 @@ describe('numbers wasm tests', () => {
it('should throw an error for DECIMAL128_PMIN divideBy DECIMAL128_MAX', async () => {
try {
await testBigDecimalDividedBy(await __newString(DECIMAL128_PMIN), await __newString(DECIMAL128_MAX));
} catch (error) {
} catch (error: any) {
expect(error.message).to.be.equal('Big decimal exponent \'-12288\' is outside the \'-6143\' to \'6144\' range');
}
});

View File

@ -31,6 +31,7 @@ import {
} from '@cerc-io/util';
import { Context, GraphData, instantiate } from './loader';
import { ObjectLiteral } from 'typeorm';
const log = debug('vulcanize:graph-watcher');
@ -281,7 +282,7 @@ export class GraphWatcher {
this._indexer = indexer;
}
async getEntity<Entity> (
async getEntity<Entity extends ObjectLiteral> (
entity: new () => Entity,
id: string,
relationsMap: Map<any, { [key: string]: any }>,
@ -305,7 +306,7 @@ export class GraphWatcher {
}
}
async getEntities<Entity> (
async getEntities<Entity extends ObjectLiteral> (
entity: new () => Entity,
relationsMap: Map<any, { [key: string]: any }>,
block: BlockHeight,

View File

@ -57,8 +57,8 @@
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",
@ -67,7 +67,7 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0",
"ts-node": "^10.0.0",
"ts-node": "^10.2.1",
"typescript": "^4.3.2"
}
}

View File

@ -4,7 +4,7 @@
import assert from 'assert';
import debug from 'debug';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import { DeepPartial, FindConditions, FindManyOptions, ObjectLiteral } from 'typeorm';
import JSONbig from 'json-bigint';
import { ethers } from 'ethers';
import { SelectionNode } from 'graphql';
@ -313,7 +313,7 @@ export class Indexer implements IndexerInterface {
await this._baseIndexer.removeStates(blockNumber, kind);
}
async getSubgraphEntity<Entity> (
async getSubgraphEntity<Entity extends ObjectLiteral> (
entity: new () => Entity,
id: string,
block: BlockHeight,
@ -324,7 +324,7 @@ export class Indexer implements IndexerInterface {
return data;
}
async getSubgraphEntities<Entity> (
async getSubgraphEntities<Entity extends ObjectLiteral> (
entity: new () => Entity,
block: BlockHeight,
where: { [key: string]: any } = {},

View File

@ -32,8 +32,8 @@
},
"devDependencies": {
"@types/ws": "^8.5.3",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",

View File

@ -55,8 +55,8 @@
"devDependencies": {
"@ethersproject/abi": "^5.3.0",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"copyfiles": "^2.4.1",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",
@ -65,7 +65,7 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0",
"ts-node": "^10.0.0",
"ts-node": "^10.2.1",
"typescript": "^4.3.2"
}
}

View File

@ -0,0 +1,2 @@
REACT_APP_SIGNAL_SERVER=/ip4/127.0.0.1/tcp/13579/ws/p2p-webrtc-star/
REACT_APP_RELAY_NODE=

23
packages/peer-test-app/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -0,0 +1,116 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Instructions
* Install dependencies:
```bash
yarn install
```
* Build the peer package:
```bash
# From repo root
cd packages/peer
yarn build
```
* (Optional) Run a local signalling server:
```bash
# In packages/peer
yarn signal-server
```
* (Optional) Create and export a peer id for the relay node:
```bash
# In packages/peer
yarn create-peer --file [PEER_ID_FILE_PATH]
```
* `file (f)`: file path to export the peer id to (json)
* (Optional) Run a local relay node:
```bash
# In packages/peer
yarn relay-node --signal-server [SIGNAL_SERVER_URL] --peer-id-file [PEER_ID_FILE_PATH]
```
* `signal-server`: multiaddr of a signalling server (default: local signalling server multiaddr)
* `peer-id-file`: file path for peer id to be used (json)
* Set the signalling server and relay node multiaddrs in the [env](./.env) file:
```
REACT_APP_SIGNAL_SERVER=/ip4/127.0.0.1/tcp/13579/ws/p2p-webrtc-star/
REACT_APP_RELAY_NODE=/ip4/127.0.0.1/tcp/13579/wss/p2p-webrtc-star/p2p/12D3KooWRzH3ZRFP6RDbs2EKA8jSrD4Y6VYtLnCRMj3mYCiMHCJP
```
* Start the react app in development mode:
```bash
# In packages/peer-test-app
yarn start
```
* The app can be opened in multiple browsers
## Development
* After making changes in [peer](../peer/) package run build
```bash
# In packages/peer
yarn build
```
* The react app server running in development mode should recompile after changes are made in peer package
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

View File

@ -0,0 +1,55 @@
{
"name": "@cerc-io/peer-test-app",
"version": "0.2.18",
"private": true,
"dependencies": {
"@cerc-io/peer": "^0.2.18",
"@cerc-io/react-peer": "^0.2.18",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@mui/material": "^5.11.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.10",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"it-pushable": "^3.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.4",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --watchAll=false",
"eject": "react-scripts eject",
"serve": "serve -s build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.18.6",
"eslint-config-react-app": "^7.0.1",
"serve": "^14.1.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>
<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>Peer Test App</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.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"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

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

View File

@ -0,0 +1,34 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,10 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
// import App from './App';
// https://github.com/facebook/create-react-app/issues/12063
xtest('renders learn react link', () => {
// render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -0,0 +1,164 @@
import React, { useContext, useEffect } from 'react';
import { PeerContext } from '@cerc-io/react-peer'
import { Peer } from '@cerc-io/peer';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { AppBar, Box, CssBaseline, Paper, Table, TableBody, TableCell, TableContainer, TableRow, Toolbar, Typography } from '@mui/material';
import './App.css';
import { useForceUpdate } from './hooks/forceUpdate';
declare global {
interface Window { broadcast: (message: string) => void; }
}
const theme = createTheme();
function App() {
const forceUpdate = useForceUpdate();
const peer: Peer = useContext(PeerContext);
useEffect(() => {
if (!peer || !peer.node) {
return
}
// Subscribe to messages from remote peers
const unsubscribeMessage = peer.subscribeMessage((peerId, message) => {
console.log(`${peerId.toString()} > ${message}`)
})
// Expose broadcast method in browser to send messages
window.broadcast = (message: string) => {
peer.broadcastMessage(message)
}
peer.node.peerStore.addEventListener('change:multiaddrs', forceUpdate)
peer.node.connectionManager.addEventListener('peer:connect', forceUpdate)
let lastDisconnect = new Date()
const disconnectHandler = () => {
forceUpdate()
const now = new Date();
const disconnectAfterSeconds = (now.getTime() - lastDisconnect.getTime()) / 1000;
console.log("Disconnected after seconds:", disconnectAfterSeconds);
lastDisconnect = now;
}
peer.node.connectionManager.addEventListener('peer:disconnect', disconnectHandler)
return () => {
unsubscribeMessage()
peer.node?.peerStore.removeEventListener('change:multiaddrs', forceUpdate)
peer.node?.connectionManager.removeEventListener('peer:connect', forceUpdate)
peer.node?.connectionManager.removeEventListener('peer:disconnect', disconnectHandler)
}
}, [peer, forceUpdate])
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<AppBar position="relative">
<Toolbar>
<Typography variant="h6" color="inherit" noWrap>
Peer Test App
</Typography>
</Toolbar>
</AppBar>
<main>
<Box
sx={{
bgcolor: 'background.paper',
py: 3,
px: 3
}}
>
<Typography variant="subtitle1" color="inherit" noWrap>
Self Node Info
</Typography>
<br/>
<TableContainer component={Paper}>
<Table>
<TableBody>
<TableRow>
<TableCell><b>Peer ID</b></TableCell>
<TableCell>{peer && peer.peerId && peer.peerId.toString()}</TableCell>
<TableCell align="right"><b>Node started</b></TableCell>
<TableCell>{peer && peer.node && peer.node.isStarted().toString()}</TableCell>
</TableRow>
<TableRow>
<TableCell><b>Signal server</b></TableCell>
<TableCell>{process.env.REACT_APP_SIGNAL_SERVER}</TableCell>
<TableCell align="right"><b>Relay node</b></TableCell>
<TableCell>{process.env.REACT_APP_RELAY_NODE}</TableCell>
</TableRow>
<TableRow>
<TableCell><b>Multiaddrs</b></TableCell>
<TableCell colSpan={3}>
<TableContainer>
<Table size="small">
<TableBody>
{
peer && peer.node && peer.node.getMultiaddrs().map(multiaddr => (
<TableRow key={multiaddr.toString()}>
<TableCell sx={{ px: 0 }}>
{multiaddr.toString()}
</TableCell>
</TableRow>
))
}
</TableBody>
</Table>
</TableContainer>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<br/>
{
peer && peer.node && (
<>
<Typography variant="subtitle1" color="inherit" noWrap>
Remote Peer Connections (Count: {peer.node.connectionManager.getConnections().length})
</Typography>
<br/>
{peer.node.connectionManager.getConnections().map(connection => (
<TableContainer sx={{ mb: 2 }} key={connection.id} component={Paper}>
<Table size="small">
<TableBody>
<TableRow>
<TableCell sx={{ width: 175 }}><b>Connection ID</b></TableCell>
<TableCell>{connection.id}</TableCell>
<TableCell align="right"><b>Direction</b></TableCell>
<TableCell>{connection.stat.direction}</TableCell>
<TableCell align="right"><b>Status</b></TableCell>
<TableCell>{connection.stat.status}</TableCell>
</TableRow>
<TableRow>
<TableCell sx={{ width: 175 }}><b>Peer ID</b></TableCell>
<TableCell colSpan={5}>{connection.remotePeer.toString()}</TableCell>
</TableRow>
<TableRow>
<TableCell sx={{ width: 175 }}><b>Connected multiaddr</b></TableCell>
<TableCell colSpan={5}>
{connection.remoteAddr.toString()}
&nbsp;
<b>{connection.remoteAddr.toString() === process.env.REACT_APP_RELAY_NODE && "(RELAY NODE)"}</b>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
))}
</>
)
}
</Box>
</main>
</ThemeProvider>
);
}
export default App;

View File

@ -0,0 +1,11 @@
//
// Copyright 2023 Vulcanize, Inc.
//
import { useState } from 'react';
export function useForceUpdate(){
const [, setValue] = useState(0); // integer state
return () => setValue(value => value + 1); // update state to force render
}

View File

@ -0,0 +1,13 @@
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;
}

View File

@ -0,0 +1,24 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { PeerProvider } from '@cerc-io/react-peer';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
// TODO: StrictMode renders component twice in development which currently causes problems with instantiating peer node.
// <React.StrictMode>
<PeerProvider signalServer={process.env.REACT_APP_SIGNAL_SERVER} relayNode={process.env.REACT_APP_RELAY_NODE}>
<App />
</PeerProvider>
// </React.StrictMode>
);
// 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();

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

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

View File

@ -0,0 +1,15 @@
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

@ -0,0 +1,5 @@
// 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

@ -0,0 +1,5 @@
//
// Copyright 2022 Vulcanize, Inc.
//
declare module '@cerc-io/react-peer';

View File

@ -0,0 +1,6 @@
{
"name": "common",
"version": "0.1.0",
"license": "AGPL-3.0",
"typings": "main.d.ts"
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

View File

@ -0,0 +1,5 @@
# Don't lint node_modules.
node_modules
# Don't lint build output.
dist

View File

@ -0,0 +1,21 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"semistandard",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/no-explicit-any": "off"
}
}

11
packages/peer/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
node_modules
#Hardhat files
cache
artifacts
#yarn
yarn-error.log
#Environment variables
.env

15
packages/peer/README.md Normal file
View File

@ -0,0 +1,15 @@
# peer
Package used for connecting between peers and send messages
## Implementations
- [x] Discover peers
- [x] Connect between peers and send messages
- [x] Use package in browser
- [x] Use package in server
- [x] Send messages between systems in different LANs using relay node
## Known Issues
- `peer:disconnect` event is not fired when remote peer browser is closed

View File

@ -0,0 +1,62 @@
{
"name": "@cerc-io/peer",
"version": "0.2.18",
"description": "libp2p module",
"main": "dist/index.js",
"exports": "./dist/index.js",
"type": "module",
"license": "AGPL-version-3.0",
"private": false,
"engines": {
"node": ">=14.16",
"npm": ">= 6.0.0"
},
"homepage": "",
"repository": {
"type": "git",
"url": ""
},
"bugs": "",
"keywords": [],
"scripts": {
"build": "tsc",
"lint": "eslint .",
"dev": "node dist/index.js",
"signal-server": "webrtc-star --port=13579 --host=0.0.0.0",
"create-peer": "node dist/create-peer.js",
"relay-node": "node dist/relay.js"
},
"dependencies": {
"@chainsafe/libp2p-noise": "^10.2.0",
"@libp2p/floodsub": "^5.0.0",
"@libp2p/interface-peer-id": "^1.1.2",
"@libp2p/mplex": "^7.1.1",
"@libp2p/peer-id-factory": "^2.0.0",
"@libp2p/pubsub-peer-discovery": "^7.0.1",
"@libp2p/webrtc-star": "^5.0.3",
"@multiformats/multiaddr": "^11.1.4",
"it-length-prefixed": "^8.0.4",
"it-map": "^2.0.0",
"it-pipe": "^2.0.5",
"it-pushable": "^3.1.2",
"libp2p": "^0.41.0",
"node-pre-gyp": "^0.13.0",
"uint8arrays": "^4.0.3",
"wrtc": "^0.4.7",
"yargs": "^17.0.1"
},
"devDependencies": {
"@libp2p/webrtc-star-signalling-server": "^2.0.5",
"@types/node": "16.11.7",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.23.3",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^5.0.0",
"typescript": "^4.9.4"
}
}

View File

@ -0,0 +1,6 @@
//
// Copyright 2023 Vulcanize, Inc.
//
export const PUBSUB_DISCOVERY_INTERVAL = 10000; // 10 seconds
export const HOP_TIMEOUT = 24 * 60 * 60 * 1000; // 1 day

View File

@ -0,0 +1,55 @@
//
// Copyright 2022 Vulcanize, Inc.
//
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import { hideBin } from 'yargs/helpers';
import yargs from 'yargs';
import { createEd25519PeerId } from '@libp2p/peer-id-factory';
interface Arguments {
file: string;
}
async function main (): Promise<void> {
const argv: Arguments = _getArgv();
const exportFilePath = path.resolve(argv.file);
const exportFileDir = path.dirname(exportFilePath);
const peerId = await createEd25519PeerId();
assert(peerId.privateKey);
const obj = {
id: peerId.toString(),
privKey: Buffer.from(peerId.privateKey).toString('base64'),
pubKey: Buffer.from(peerId.publicKey).toString('base64')
};
if (!fs.existsSync(exportFileDir)) {
fs.mkdirSync(exportFileDir, { recursive: true });
}
fs.writeFileSync(exportFilePath, JSON.stringify(obj));
console.log(`Peer id ${peerId.toString()} exported to file ${exportFilePath}`);
}
function _getArgv (): any {
return yargs(hideBin(process.argv)).parserConfiguration({
'parse-numbers': false
}).options({
file: {
type: 'string',
alias: 'f',
describe: 'Peer Id export file path (json)',
demandOption: true
}
}).argv;
}
main().catch(err => {
console.log(err);
});

276
packages/peer/src/index.ts Normal file
View File

@ -0,0 +1,276 @@
//
// Copyright 2022 Vulcanize, Inc.
//
import { createLibp2p, Libp2p } from 'libp2p';
// For nodejs.
import wrtc from 'wrtc';
import assert from 'assert';
import { pipe } from 'it-pipe';
import * as lp from 'it-length-prefixed';
import map from 'it-map';
import { pushable, Pushable } from 'it-pushable';
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string';
import { toString as uint8ArrayToString } from 'uint8arrays/to-string';
import { webRTCStar, WebRTCStarTuple } from '@libp2p/webrtc-star';
import { noise } from '@chainsafe/libp2p-noise';
import { mplex } from '@libp2p/mplex';
import type { Stream as P2PStream, Connection } from '@libp2p/interface-connection';
import type { PeerInfo } from '@libp2p/interface-peer-info';
import { PeerId } from '@libp2p/interface-peer-id';
import { multiaddr, Multiaddr } from '@multiformats/multiaddr';
import { floodsub } from '@libp2p/floodsub';
import { pubsubPeerDiscovery } from '@libp2p/pubsub-peer-discovery';
import { PUBSUB_DISCOVERY_INTERVAL } from './constants.js';
export const CHAT_PROTOCOL = '/chat/1.0.0';
export const DEFAULT_SIGNAL_SERVER_URL = '/ip4/127.0.0.1/tcp/13579/wss/p2p-webrtc-star';
export class Peer {
_node?: Libp2p
_wrtcStar: WebRTCStarTuple
_relayNodeMultiaddr?: Multiaddr
_remotePeerIds: PeerId[] = []
_peerStreamMap: Map<string, Pushable<any>> = new Map()
_messageHandlers: Array<(peerId: PeerId, message: any) => void> = []
constructor (nodejs?: boolean) {
// Instantiation in nodejs.
if (nodejs) {
this._wrtcStar = webRTCStar({ wrtc });
} else {
this._wrtcStar = webRTCStar();
}
}
get peerId (): PeerId | undefined {
return this._node?.peerId;
}
get node (): Libp2p | undefined {
return this._node;
}
async init (signalServerURL = DEFAULT_SIGNAL_SERVER_URL, relayNodeURL?: string): Promise<void> {
let peerDiscovery: any;
if (relayNodeURL) {
this._relayNodeMultiaddr = multiaddr(relayNodeURL);
peerDiscovery = [
pubsubPeerDiscovery({
interval: PUBSUB_DISCOVERY_INTERVAL
})
];
} else {
peerDiscovery = [this._wrtcStar.discovery];
}
this._node = await createLibp2p({
addresses: {
// Add the signaling server address, along with our PeerId to our multiaddrs list
// libp2p will automatically attempt to dial to the signaling server so that it can
// receive inbound connections from other peers
listen: [
// Public signal servers
// '/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star',
// '/dns4/wrtc-star2.sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star'
signalServerURL
]
},
transports: [
this._wrtcStar.transport
],
connectionEncryption: [noise()],
streamMuxers: [mplex()],
pubsub: floodsub(),
peerDiscovery,
relay: {
enabled: true,
autoRelay: {
enabled: true,
maxListeners: 2
}
},
connectionManager: {
maxDialsPerPeer: 3, // Number of max concurrent dials per peer
autoDial: false
}
});
console.log('libp2p node created', this._node);
// Dial to the HOP enabled relay node if available
if (this._relayNodeMultiaddr) {
const relayMultiaddr = this._relayNodeMultiaddr;
console.log(`Dialling relay node ${relayMultiaddr.getPeerId()} using multiaddr ${relayMultiaddr.toString()}`);
await this._node.dial(relayMultiaddr);
}
// Listen for change in stored multiaddrs
this._node.peerStore.addEventListener('change:multiaddrs', (evt) => {
assert(this._node);
const { peerId, multiaddrs } = evt.detail;
// Log updated self multiaddrs
if (peerId.equals(this._node.peerId)) {
console.log('Updated self multiaddrs', this._node.getMultiaddrs().map(addr => addr.toString()));
} else {
console.log('Updated peer node multiaddrs', multiaddrs.map((addr: Multiaddr) => addr.toString()));
}
});
// Listen for peers discovery
this._node.addEventListener('peer:discovery', (evt) => {
// console.log('event peer:discovery', evt);
this._handleDiscovery(evt.detail);
});
// Listen for peers connection
this._node.connectionManager.addEventListener('peer:connect', (evt) => {
console.log('event peer:connect', evt);
this._handleConnect(evt.detail);
});
// Listen for peers disconnecting
this._node.connectionManager.addEventListener('peer:disconnect', (evt) => {
console.log('event peer:disconnect', evt);
this._handleDisconnect(evt.detail);
});
// Handle messages for the protocol
await this._node.handle(CHAT_PROTOCOL, async ({ stream, connection }) => {
this._handleStream(connection.remotePeer, stream);
});
}
async close (): Promise<void> {
assert(this._node);
this._node.removeEventListener('peer:discovery');
this._node.connectionManager.removeEventListener('peer:connect');
this._node.connectionManager.removeEventListener('peer:disconnect');
await this._node.unhandle(CHAT_PROTOCOL);
const hangUpPromises = this._remotePeerIds.map(async peerId => this._node?.hangUp(peerId));
await Promise.all(hangUpPromises);
}
broadcastMessage (message: any): void {
for (const [, stream] of this._peerStreamMap) {
stream.push(message);
}
}
subscribeMessage (handler: (peerId: PeerId, message: any) => void) : () => void {
this._messageHandlers.push(handler);
const unsubscribe = () => {
this._messageHandlers = this._messageHandlers
.filter(registeredHandler => registeredHandler !== handler);
};
return unsubscribe;
}
_handleDiscovery (peer: PeerInfo): void {
// Check connected peers as they are discovered repeatedly.
if (!this._remotePeerIds.some(remotePeerId => remotePeerId.toString() === peer.id.toString())) {
console.log('Discovered peer multiaddrs', peer.multiaddrs.map(addr => addr.toString()));
this._connectPeer(peer);
}
}
_handleConnect (connection: Connection): void {
const remotePeerId = connection.remotePeer;
this._remotePeerIds.push(remotePeerId);
// Log connected peer
console.log(`Connected to ${remotePeerId.toString()} using multiaddr ${connection.remoteAddr.toString()}`);
}
_handleDisconnect (connection: Connection): void {
const disconnectedPeerId = connection.remotePeer;
this._remotePeerIds = this._remotePeerIds.filter(remotePeerId => remotePeerId.toString() !== disconnectedPeerId.toString());
// Log disconnected peer
console.log(`Disconnected from ${disconnectedPeerId.toString()} using multiaddr ${connection.remoteAddr.toString()}`);
}
async _connectPeer (peer: PeerInfo): Promise<void> {
assert(this._node);
// Check if discovered the relay node
if (this._relayNodeMultiaddr) {
const relayMultiaddr = this._relayNodeMultiaddr;
const relayNodePeerId = relayMultiaddr.getPeerId();
if (relayNodePeerId && relayNodePeerId === peer.id.toString()) {
console.log(`Dialling relay peer ${peer.id.toString()} using multiaddr ${relayMultiaddr.toString()}`);
await this._node.dial(relayMultiaddr).catch(err => {
console.log(`Could not dial relay ${relayMultiaddr.toString()}`, err);
});
return;
}
}
// Dial them when we discover them
// Attempt to dial all the multiaddrs of the discovered peer (to connect through relay)
for (const peerMultiaddr of peer.multiaddrs) {
try {
console.log(`Dialling peer ${peer.id.toString()} using multiaddr ${peerMultiaddr.toString()}`);
const stream = await this._node.dialProtocol(peerMultiaddr, CHAT_PROTOCOL);
this._handleStream(peer.id, stream);
break;
} catch (err) {
console.log(`Could not dial ${peerMultiaddr.toString()}`, err);
}
}
}
_handleStream (peerId: PeerId, stream: P2PStream): void {
// console.log('Stream after connection', stream);
const messageStream = pushable<any>({ objectMode: true });
// Send message to pipe from stdin
pipe(
// Read from stream (the source)
messageStream,
// Turn objects into buffers
(source) => map(source, (value) => {
return uint8ArrayFromString(JSON.stringify(value));
}),
// Encode with length prefix (so receiving side knows how much data is coming)
lp.encode(),
// Write to the stream (the sink)
stream.sink
);
// Handle message from stream
pipe(
// Read from the stream (the source)
stream.source,
// Decode length-prefixed data
lp.decode(),
// Turn buffers into objects
(source) => map(source, (buf) => {
return JSON.parse(uint8ArrayToString(buf.subarray()));
}),
// Sink function
async (source) => {
// For each chunk of data
for await (const msg of source) {
this._messageHandlers.forEach(messageHandler => messageHandler(peerId, msg));
}
}
);
// TODO: Check if stream already exists for peer id
this._peerStreamMap.set(peerId.toString(), messageStream);
}
}

View File

@ -0,0 +1,98 @@
//
// Copyright 2022 Vulcanize, Inc.
//
import { createLibp2p } from 'libp2p';
import wrtc from 'wrtc';
import { hideBin } from 'yargs/helpers';
import yargs from 'yargs';
import fs from 'fs';
import path from 'path';
import { noise } from '@chainsafe/libp2p-noise';
import { mplex } from '@libp2p/mplex';
import { webRTCStar, WebRTCStarTuple } from '@libp2p/webrtc-star';
import { floodsub } from '@libp2p/floodsub';
import { pubsubPeerDiscovery } from '@libp2p/pubsub-peer-discovery';
import { createFromJSON } from '@libp2p/peer-id-factory';
import { DEFAULT_SIGNAL_SERVER_URL } from './index.js';
import { HOP_TIMEOUT, PUBSUB_DISCOVERY_INTERVAL } from './constants.js';
interface Arguments {
signalServer: string;
peerIdFile: string;
}
async function main (): Promise<void> {
const argv: Arguments = _getArgv();
if (!argv.signalServer) {
console.log('Using the default signalling server URL');
}
let peerId: any;
if (argv.peerIdFile) {
const peerIdFilePath = path.resolve(argv.peerIdFile);
console.log(`Reading peer id from file ${peerIdFilePath}`);
const peerIdObj = fs.readFileSync(peerIdFilePath, 'utf-8');
const peerIdJson = JSON.parse(peerIdObj);
peerId = await createFromJSON(peerIdJson);
} else {
console.log('Creating a new peer id');
}
const wrtcStar: WebRTCStarTuple = webRTCStar({ wrtc });
const node = await createLibp2p({
peerId,
addresses: {
listen: [
argv.signalServer || DEFAULT_SIGNAL_SERVER_URL
]
},
transports: [
wrtcStar.transport
],
connectionEncryption: [noise()],
streamMuxers: [mplex()],
pubsub: floodsub(),
peerDiscovery: [
pubsubPeerDiscovery({
interval: PUBSUB_DISCOVERY_INTERVAL
})
],
relay: {
enabled: true,
hop: {
enabled: true,
timeout: HOP_TIMEOUT
},
advertise: {
enabled: true
}
}
});
console.log(`Relay node started with id ${node.peerId.toString()}`);
console.log('Listening on:');
node.getMultiaddrs().forEach((ma) => console.log(ma.toString()));
}
function _getArgv (): any {
return yargs(hideBin(process.argv)).parserConfiguration({
'parse-numbers': false
}).options({
signalServer: {
type: 'string',
describe: 'Signalling server URL'
},
peerIdFile: {
type: 'string',
describe: 'Relay Peer Id file path (json)'
}
}).argv;
}
main().catch(err => {
console.log(err);
});

View File

@ -0,0 +1,5 @@
//
// Copyright 2022 Vulcanize, Inc.
//
declare module 'wrtc';

View File

@ -0,0 +1,6 @@
{
"name": "common",
"version": "0.1.0",
"license": "AGPL-3.0",
"typings": "main.d.ts"
}

View File

@ -0,0 +1,78 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"module": "node16", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": [ "ES5", "ES6", "ES2020" ], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "dist", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
"downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
"moduleResolution": "node16", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": [
"./src/types",
"node_modules/@types"
], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
"resolveJsonModule": true /* Enabling the option allows importing JSON, and validating the types in that JSON file. */
},
"include": ["src"],
"exclude": ["test", "dist", "artifacts", "cache"]
}

7
packages/react-peer/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
/coverage
/demo/dist
/es
/lib
/node_modules
/umd
npm-debug.log*

View File

@ -0,0 +1,16 @@
sudo: false
language: node_js
node_js:
- 10
before_install:
- npm install codecov.io coveralls
after_success:
- cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js
- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
branches:
only:
- master

View File

@ -0,0 +1,25 @@
## Prerequisites
[Node.js](http://nodejs.org/) >= 10 must be installed.
## Installation
- Running `npm install` in the component's root directory will install everything you need for development.
## Demo Development Server
- `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading.
## Running Tests
- `npm test` will run the tests once.
- `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`.
- `npm run test:watch` will run the tests on every change.
## Building
- `npm run build` will build the component for publishing to npm and also bundle the demo app.
- `npm run clean` will delete built resources.

View File

@ -0,0 +1,16 @@
# react-peer
[![Travis][build-badge]][build]
[![npm package][npm-badge]][npm]
[![Coveralls][coveralls-badge]][coveralls]
Describe react-peer here.
[build-badge]: https://img.shields.io/travis/user/repo/master.png?style=flat-square
[build]: https://travis-ci.org/user/repo
[npm-badge]: https://img.shields.io/npm/v/npm-package.png?style=flat-square
[npm]: https://www.npmjs.org/package/npm-package
[coveralls-badge]: https://img.shields.io/coveralls/user/repo/master.png?style=flat-square
[coveralls]: https://coveralls.io/github/user/repo

16
packages/react-peer/demo/src/index.js vendored Normal file
View File

@ -0,0 +1,16 @@
import React, {Component} from 'react'
import {render} from 'react-dom'
import { PeerProvider } from '../../src'
export default class Demo extends Component {
render() {
return <PeerProvider>
<div>
<h1>react-peer Demo</h1>
</div>
</PeerProvider>
}
}
render(<Demo/>, document.querySelector('#demo'))

View File

@ -0,0 +1,7 @@
module.exports = {
type: 'react-component',
npm: {
esModules: false,
umd: false
}
}

View File

@ -0,0 +1,38 @@
{
"name": "@cerc-io/react-peer",
"version": "0.2.18",
"description": "react-peer React component",
"main": "lib/index.js",
"files": [
"css",
"es",
"lib",
"umd"
],
"scripts": {
"build": "nwb build-react-component --no-demo",
"clean": "nwb clean-module && nwb clean-demo",
"prepublishOnly": "npm run build",
"start": "nwb serve-react-demo",
"test:coverage": "nwb test-react --coverage",
"test:watch": "nwb test-react --server"
},
"dependencies": {
"@cerc-io/peer": "^0.2.18"
},
"peerDependencies": {
"react": "^18.2.0"
},
"devDependencies": {
"nwb": "0.25.x",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"author": "",
"homepage": "",
"license": "MIT",
"repository": "",
"keywords": [
"react-component"
]
}

View File

@ -0,0 +1,3 @@
import React from "react";
export const PeerContext = React.createContext({});

View File

@ -0,0 +1,36 @@
import React from 'react';
import { Peer } from '@cerc-io/peer';
import { PeerContext } from './PeerContext';
export const PeerProvider = ({ signalServer, relayNode, children }) => {
const [peer, setPeer] = React.useState(null);
React.useEffect(() => {
const init = async () => {
const peer = new Peer()
await peer.init(signalServer, relayNode);
// Debug
console.log(`Peer ID: ${peer.peerId.toString()}`);
setPeer(peer);
};
init();
return () => {
if (peer.node) {
// TODO: Await for peer close
peer.close()
}
}
}, []);
return (
<PeerContext.Provider value={peer}>
{children}
</PeerContext.Provider>
);
};

2
packages/react-peer/src/index.js vendored Normal file
View File

@ -0,0 +1,2 @@
export { PeerContext } from './context/PeerContext'
export { PeerProvider } from './context/PeerProvider'

View File

@ -9,8 +9,8 @@
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@types/chai": "^4.2.18",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"chai": "^4.3.4",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",

View File

@ -23,10 +23,10 @@
"devDependencies": {
"@types/chai": "^4.2.19",
"@types/mocha": "^8.2.2",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"eslint": "^7.27.0",
"ts-node": "^10.0.0",
"ts-node": "^10.2.1",
"typescript": "^4.3.2"
}
}

View File

@ -24,8 +24,8 @@
"yargs": "^17.0.1"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",
"eslint-config-standard": "^16.0.3",

View File

@ -45,8 +45,8 @@
"@types/js-yaml": "^4.0.4",
"@types/pg": "^8.6.5",
"@types/ws": "^8.5.3",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"decimal.js": "^10.3.1",
"eslint": "^7.27.0",
"eslint-config-semistandard": "^15.0.1",

View File

@ -13,6 +13,7 @@ import {
FindConditions,
FindManyOptions,
In,
ObjectLiteral,
QueryRunner,
Repository,
SelectQueryBuilder
@ -427,7 +428,7 @@ export class Database {
return event;
}
async getFrothyEntity<Entity> (queryRunner: QueryRunner, repo: Repository<Entity>, data: { blockHash: string, id: string }): Promise<{ blockHash: string, blockNumber: number, id: string }> {
async getFrothyEntity<Entity extends ObjectLiteral> (queryRunner: QueryRunner, repo: Repository<Entity>, data: { blockHash: string, id: string }): Promise<{ blockHash: string, blockNumber: number, id: string }> {
// Hierarchical query for getting the entity in the frothy region.
const heirerchicalQuery = `
WITH RECURSIVE cte_query AS
@ -478,7 +479,7 @@ export class Database {
return { blockHash, blockNumber, id };
}
async getPrevEntityVersion<Entity> (queryRunner: QueryRunner, repo: Repository<Entity>, findOptions: { [key: string]: any }): Promise<Entity | undefined> {
async getPrevEntityVersion<Entity extends ObjectLiteral> (queryRunner: QueryRunner, repo: Repository<Entity>, findOptions: { [key: string]: any }): Promise<Entity | undefined> {
const { blockHash, blockNumber, id } = await this.getFrothyEntity(queryRunner, repo, findOptions.where);
if (id) {
@ -491,7 +492,7 @@ export class Database {
return this.getLatestPrunedEntity(repo, findOptions.where.id, blockNumber + 1);
}
async getLatestPrunedEntity<Entity> (repo: Repository<Entity>, id: string, canonicalBlockNumber: number): Promise<Entity | undefined> {
async getLatestPrunedEntity<Entity extends ObjectLiteral> (repo: Repository<Entity>, id: string, canonicalBlockNumber: number): Promise<Entity | undefined> {
// Filter out latest entity from pruned blocks.
const entityInPrunedRegion = await repo.createQueryBuilder('entity')
.where('entity.id = :id', { id })
@ -797,7 +798,7 @@ export class Database {
return repo.save(entity);
}
buildQuery<Entity> (
buildQuery<Entity extends ObjectLiteral> (
repo: Repository<Entity>,
selectQueryBuilder: SelectQueryBuilder<Entity>,
where: Where = {},
@ -867,7 +868,7 @@ export class Database {
return selectQueryBuilder;
}
orderQuery<Entity> (
orderQuery<Entity extends ObjectLiteral> (
repo: Repository<Entity>,
selectQueryBuilder: SelectQueryBuilder<Entity>,
orderOptions: { orderBy?: string, orderDirection?: string },

View File

@ -3,9 +3,9 @@ import { utils } from 'ethers';
const log = debug('vulcanize:eth');
function decodeInteger(value : string, defaultValue: BigInt): BigInt
function decodeInteger(value : string) : BigInt | undefined
function decodeInteger (value : string, defaultValue?: BigInt): BigInt | undefined {
function decodeInteger(value : string, defaultValue: bigint): bigint
function decodeInteger(value : string) : bigint | undefined
function decodeInteger (value : string, defaultValue?: bigint): bigint | undefined {
if (value === undefined || value === null || value.length === 0) return defaultValue;
if (value === '0x') return BigInt(0);
return BigInt(value);

View File

@ -13,7 +13,8 @@ import {
QueryRunner,
Repository,
SelectQueryBuilder,
UpdateResult
UpdateResult,
ObjectLiteral
} from 'typeorm';
import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer';
@ -93,7 +94,7 @@ export class GraphDatabase {
return this._baseDatabase.createTransactionRunner();
}
async getModelEntity<Entity> (repo: Repository<Entity>, whereOptions: any): Promise<Entity | undefined> {
async getModelEntity<Entity extends ObjectLiteral> (repo: Repository<Entity>, whereOptions: any): Promise<Entity | undefined> {
eventProcessingLoadEntityCount.inc();
const findOptions = {
@ -165,7 +166,7 @@ export class GraphDatabase {
return repo.findOne(findOptions);
}
async getEntity<Entity> (entityName: string, id: string, blockHash?: string): Promise<Entity | undefined> {
async getEntity<Entity extends ObjectLiteral> (entityName: string, id: string, blockHash?: string): Promise<Entity | undefined> {
const queryRunner = this._conn.createQueryRunner();
try {
@ -212,7 +213,7 @@ export class GraphDatabase {
return count > 0;
}
async getEntityWithRelations<Entity> (
async getEntityWithRelations<Entity extends ObjectLiteral> (
queryRunner: QueryRunner,
entityType: (new () => Entity),
id: string,
@ -343,7 +344,7 @@ export class GraphDatabase {
return entityData;
}
async getEntities<Entity> (
async getEntities<Entity extends ObjectLiteral> (
queryRunner: QueryRunner,
entityType: new () => Entity,
relationsMap: Map<any, { [key: string]: any }>,
@ -411,7 +412,7 @@ export class GraphDatabase {
return entities;
}
async getEntitiesGroupBy<Entity> (
async getEntitiesGroupBy<Entity extends ObjectLiteral> (
queryRunner: QueryRunner,
entityType: new () => Entity,
block: BlockHeight,
@ -475,7 +476,7 @@ export class GraphDatabase {
return entities;
}
async getEntitiesDistinctOn<Entity> (
async getEntitiesDistinctOn<Entity extends ObjectLiteral> (
queryRunner: QueryRunner,
entityType: new () => Entity,
block: BlockHeight,
@ -516,7 +517,7 @@ export class GraphDatabase {
`(${subQuery.getQuery()})`,
'latestEntities'
)
.setParameters(subQuery.getParameters());
.setParameters(subQuery.getParameters()) as SelectQueryBuilder<Entity>;
if (queryOptions.orderBy) {
selectQueryBuilder = this._baseDatabase.orderQuery(repo, selectQueryBuilder, queryOptions, 'subTable_');
@ -539,7 +540,7 @@ export class GraphDatabase {
return entities as Entity[];
}
async getEntitiesSingular<Entity> (
async getEntitiesSingular<Entity extends ObjectLiteral> (
queryRunner: QueryRunner,
entityType: new () => Entity,
block: BlockHeight,
@ -574,7 +575,7 @@ export class GraphDatabase {
return entities as Entity[];
}
async getEntitiesUnique<Entity> (
async getEntitiesUnique<Entity extends ObjectLiteral> (
queryRunner: QueryRunner,
entityType: new () => Entity,
block: BlockHeight,
@ -676,7 +677,7 @@ export class GraphDatabase {
return selectQueryBuilder.getMany();
}
async getEntitiesLateral<Entity> (
async getEntitiesLateral<Entity extends ObjectLiteral> (
queryRunner: QueryRunner,
entityType: new () => Entity,
latestEntity: new () => any,
@ -717,7 +718,7 @@ export class GraphDatabase {
return qb;
},
'result'
);
) as SelectQueryBuilder<Entity>;
selectQueryBuilder = this._baseDatabase.buildQuery(latestEntityRepo, selectQueryBuilder, where, 'latest');
@ -1040,11 +1041,11 @@ export class GraphDatabase {
}
cacheUpdatedEntityByName (entityName: string, entity: any, pruned = false): void {
const repo = this._conn.getRepository(entityName);
const repo = this._conn.getRepository<ObjectLiteral>(entityName);
this.cacheUpdatedEntity(repo, entity, pruned);
}
cacheUpdatedEntity<Entity> (repo: Repository<Entity>, entity: any, pruned = false): void {
cacheUpdatedEntity<Entity extends ObjectLiteral> (repo: Repository<Entity>, entity: any, pruned = false): void {
const tableName = repo.metadata.tableName;
if (pruned) {
@ -1204,8 +1205,8 @@ export class GraphDatabase {
}
async canonicalizeLatestEntity (queryRunner: QueryRunner, entityType: any, latestEntityType: any, entities: any[], blockNumber: number): Promise<void> {
const repo = queryRunner.manager.getRepository(entityType);
const latestEntityRepo = queryRunner.manager.getRepository(latestEntityType);
const repo = queryRunner.manager.getRepository<ObjectLiteral>(entityType);
const latestEntityRepo = queryRunner.manager.getRepository<ObjectLiteral>(latestEntityType);
await Promise.all(entities.map(async (entity: any) => {
// Get latest pruned (canonical) version for the given entity

View File

@ -3,7 +3,7 @@ import path from 'path';
import fs from 'fs-extra';
import debug from 'debug';
import yaml from 'js-yaml';
import { DeepPartial, EntityTarget, InsertEvent, Repository, UpdateEvent } from 'typeorm';
import { DeepPartial, EntityTarget, InsertEvent, ObjectLiteral, Repository, UpdateEvent } from 'typeorm';
import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
import assert from 'assert';
import _ from 'lodash';
@ -855,7 +855,7 @@ export const afterEntityInsertOrUpdate = async<Entity> (
.execute();
};
export function getLatestEntityFromEntity<Entity> (latestEntityRepo: Repository<Entity>, entity: any): Entity {
export function getLatestEntityFromEntity<Entity extends ObjectLiteral> (latestEntityRepo: Repository<Entity>, entity: any): Entity {
const latestEntityFields = latestEntityRepo.metadata.columns.map(column => column.propertyName);
return latestEntityRepo.create(_.pick(entity, latestEntityFields) as DeepPartial<Entity>);
}

View File

@ -2,7 +2,7 @@
// Copyright 2021 Vulcanize, Inc.
//
import { Connection, DeepPartial, EntityTarget, FindConditions, FindManyOptions, QueryRunner } from 'typeorm';
import { Connection, DeepPartial, EntityTarget, FindConditions, FindManyOptions, ObjectLiteral, QueryRunner } from 'typeorm';
import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper';
import { EthClient } from '@cerc-io/ipld-eth-client';
@ -182,7 +182,7 @@ export interface DatabaseInterface {
}
export interface GraphDatabaseInterface {
getEntity<Entity> (entity: (new () => Entity) | string, id: string, blockHash?: string): Promise<Entity | undefined>;
getEntity<Entity extends ObjectLiteral> (entity: (new () => Entity) | string, id: string, blockHash?: string): Promise<Entity | undefined>;
}
export interface GraphWatcherInterface {

17903
yarn.lock

File diff suppressed because it is too large Load Diff