diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..9dcd433f --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint diff --git a/package.json b/package.json index 12c53be6..0d908f14 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,15 @@ "nohoist": ["**/mocha", "**/mocha/**"] }, "devDependencies": { - "lerna": "^6.6.2" + "lerna": "^6.6.2", + "husky": "^7.0.2" }, "scripts": { "lint": "lerna run lint --stream", - "build:browser": "lerna run build:browser,build --stream", - "build:node": "lerna run build:node,build --stream --ignore @cerc-io/example-web-app", - "test:browser": "lerna run test --stream", - "test:node": "lerna run test --stream --ignore @cerc-io/example-web-app" + "build:browser": "TARGET=browser lerna run build --stream --ignore @cerc-io/server", + "build:node": "TARGET=node lerna run build --stream --ignore @cerc-io/example-web-app", + "test:browser": "lerna run test --stream --ignore @cerc-io/server", + "test:node": "lerna run test --stream --ignore @cerc-io/example-web-app", + "prepare": "husky install" } } diff --git a/packages/nitro-client/package.json b/packages/nitro-client/package.json index 24fcdbe5..ced77803 100644 --- a/packages/nitro-client/package.json +++ b/packages/nitro-client/package.json @@ -6,6 +6,7 @@ "scripts": { "index:browser": "cp src/browser.ts src/index.ts", "index:node": "cp src/node.ts src/index.ts", + "build": "if [ \"$TARGET\" = \"browser\" ]; then yarn build:browser; else yarn build:node; fi", "build:browser": "yarn index:browser && webpack --config webpack.prod.ts --env target=browser", "build:node": "yarn index:node && webpack --config webpack.prod.ts --env target=node", "build:dev:browser": "yarn index:browser && webpack --config webpack.dev.ts --env target=browser", diff --git a/packages/nitro-client/src/client/engine/messageservice/p2p-message-service/service.ts b/packages/nitro-client/src/client/engine/messageservice/p2p-message-service/service.ts index 3e5cb558..93e16400 100644 --- a/packages/nitro-client/src/client/engine/messageservice/p2p-message-service/service.ts +++ b/packages/nitro-client/src/client/engine/messageservice/p2p-message-service/service.ts @@ -17,7 +17,7 @@ import type { Stream } from '@libp2p/interface-connection'; // @ts-expect-error import type { IncomingStreamData } from '@libp2p/interface-registrar'; // @ts-expect-error -import type { Multiaddr } from '@multiformats/multiaddr'; +import type { PeerInfo as Libp2pPeerInfo } from '@libp2p/interface-peer-info'; import { SyncMap } from '../../../../internal/safesync/safesync'; import { Message } from '../../../../protocols/messages'; @@ -96,14 +96,13 @@ export class P2PMessageService { // newMessageService returns a running P2PMessageService listening on the given ip, port and message key. // If useMdnsPeerDiscovery is true, the message service will use mDNS to discover peers. // Otherwise, peers must be added manually via `AddPeers`. - // TODO: Implement and remove void static async newMessageService( ip: string, port: number, me: Address, pk: Uint8Array, useMdnsPeerDiscovery: boolean, - logWriter: WritableStream, + logWriter?: WritableStream, ): Promise { const ms = new P2PMessageService({ toEngine: Channel(BUFFER_SIZE), @@ -118,7 +117,7 @@ export class P2PMessageService { try { const messageKey = await unmarshalPrivateKey(pk); ms.key = messageKey; - } catch (err: unknown) { + } catch (err) { ms.checkError(err as Error); } @@ -155,6 +154,8 @@ export class P2PMessageService { const host = await createLibp2p(options); ms.p2pHost = host; + ms.p2pHost.addEventListener('peer:discovery', ms.handlePeerFound.bind(ms)); + ms.p2pHost.handle(PROTOCOL_ID, ms.msgStreamHandler); ms.p2pHost.handle(PEER_EXCHANGE_PROTOCOL_ID, ({ stream }) => { @@ -170,8 +171,30 @@ export class P2PMessageService { id(): string | void {} // handlePeerFound is called by the mDNS service when a peer is found. - // TODO: Implement and remove void - handlePeerFound(pi: Multiaddr[]) {} + async handlePeerFound({ detail: pi }: CustomEvent) { + assert(this.p2pHost); + + const peer = await this.p2pHost.peerStore.save( + pi.id, + { + multiaddrs: pi.multiaddrs, + // TODO: Check if ttl option exists to set it like in go-nitro + // peerstore.PermanentAddrTTL + }, + ); + + try { + const stream = await this.p2pHost.dialProtocol( + peer.id, + PEER_EXCHANGE_PROTOCOL_ID, + ); + + this.sendPeerInfo(stream); + stream.close(); + } catch (err) { + this.checkError(err as Error); + } + } // TODO: Implement private msgStreamHandler({ stream }: IncomingStreamData) {} diff --git a/packages/nitro-client/webpack.common.ts b/packages/nitro-client/webpack.common.ts index 4b1d1731..a3b282e0 100644 --- a/packages/nitro-client/webpack.common.ts +++ b/packages/nitro-client/webpack.common.ts @@ -27,19 +27,22 @@ const baseConfig: webpack.Configuration = { ], }, externals: { - '@chainsafe/libp2p-yamux': '@chainsafe/libp2p-yamux', - '@libp2p/crypto': '@libp2p/crypto', - '@libp2p/mdns': '@libp2p/mdns', - '@libp2p/tcp': '@libp2p/tcp', '@nodeguy/channel': '@nodeguy/channel', debug: 'debug', ethers: 'ethers', - libp2p: 'libp2p', }, }; export const browserConfig: webpack.Configuration = merge(baseConfig, { entry: './src/browser.ts', + // Packages are resolved properly in browser build tool; so not required in build output + externals: { + '@chainsafe/libp2p-yamux': '@chainsafe/libp2p-yamux', + '@libp2p/crypto': '@libp2p/crypto', + '@libp2p/mdns': '@libp2p/mdns', + '@libp2p/tcp': '@libp2p/tcp', + libp2p: 'libp2p', + }, }); export const nodeConfig: webpack.Configuration = merge(baseConfig, { diff --git a/packages/server/.eslintignore b/packages/server/.eslintignore new file mode 100644 index 00000000..653874b5 --- /dev/null +++ b/packages/server/.eslintignore @@ -0,0 +1,5 @@ +# Don't lint node_modules. +node_modules + +# Don't lint build output. +dist diff --git a/packages/server/.eslintrc.json b/packages/server/.eslintrc.json new file mode 100644 index 00000000..ed6cfd52 --- /dev/null +++ b/packages/server/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": [ + "semistandard", + "airbnb-base", + "airbnb-typescript/base" + ], + "parserOptions": { + "project": "./tsconfig.eslint.json" + }, + "rules": { + "import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": ["test/**/*.test.ts"] + } + ] + } +} diff --git a/packages/server/.gitignore b/packages/server/.gitignore new file mode 100644 index 00000000..7d4f0ad5 --- /dev/null +++ b/packages/server/.gitignore @@ -0,0 +1,3 @@ +node_modules/ + +dist diff --git a/packages/server/package.json b/packages/server/package.json new file mode 100644 index 00000000..8b12f25a --- /dev/null +++ b/packages/server/package.json @@ -0,0 +1,30 @@ +{ + "name": "@cerc-io/server", + "version": "0.1.0", + "main": "index.js", + "license": "MIT", + "private": true, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^5.13.0", + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^7.32.0 || ^8.2.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^17.0.0", + "eslint-config-semistandard": "^17.0.0", + "eslint-config-standard": "^17.0.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-n": "^15.0.0", + "eslint-plugin-promise": "^6.0.0", + "typescript": "^5.0.4" + }, + "scripts": { + "build": "tsc", + "start": "DEBUG=ts-nitro:* node --enable-source-maps dist/index.js", + "lint": "eslint ." + }, + "dependencies": { + "debug": "^4.3.4", + "yargs": "^17.7.2", + "@cerc-io/nitro-client": "^0.1.0" + } +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts new file mode 100644 index 00000000..6f088d09 --- /dev/null +++ b/packages/server/src/index.ts @@ -0,0 +1,48 @@ +import yargs from 'yargs'; +import debug from 'debug'; + +import { P2PMessageService } from '@cerc-io/nitro-client'; + +const log = debug('ts-nitro:server'); + +const getArgv = () => yargs.parserConfiguration({ + 'parse-numbers': false, +}).options({ + port: { + alias: 'p', + type: 'number', + require: true, + demandOption: true, + describe: 'Message service port', + }, +}).argv; + +const main = async () => { + const argv = getArgv(); + + const keys = await import('@libp2p/crypto/keys'); + + // TODO: Generate private key from a string + const privateKey = await keys.generateKeyPair('Ed25519'); + + const p2pMessageService = await P2PMessageService.newMessageService( + '127.0.0.1', + argv.port, + // TODO: Pass account address + '', + privateKey.bytes, + true, + ); + + log('p2pMessageService', p2pMessageService.constructor.name); +}; + +main().then(() => { + log('Started P2PMessageService'); +}).catch((err) => { + log(err); +}); + +process.on('uncaughtException', (err) => { + log('uncaughtException', err); +}); diff --git a/packages/server/tsconfig.eslint.json b/packages/server/tsconfig.eslint.json new file mode 100644 index 00000000..25b5ba21 --- /dev/null +++ b/packages/server/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts", "test/**/*.test.ts"] +} diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json new file mode 100644 index 00000000..19766e44 --- /dev/null +++ b/packages/server/tsconfig.json @@ -0,0 +1,103 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "CommonJS", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node16", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/yarn.lock b/yarn.lock index 9f177b70..1a38aa0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7760,6 +7760,11 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" +husky@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" + integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== + iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" @@ -14571,9 +14576,9 @@ yargs@16.2.0, yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.6.2: +yargs@^17.6.2, yargs@^17.7.2: version "17.7.2" - resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: cliui "^8.0.1"