diff --git a/package.json b/package.json index 38f96612..216249aa 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,6 @@ "node": ">=10", "pnpm": ">=3" }, - "scripts": { - "simulate-cdn": "http-server -p 8765 ./packages/client/js-client.web.standalone/dist" - }, "author": "Fluence Labs", "license": "Apache-2.0", "devDependencies": { diff --git a/packages/@tests/aqua/src/index.ts b/packages/@tests/aqua/src/index.ts index 217bd035..628fb112 100644 --- a/packages/@tests/aqua/src/index.ts +++ b/packages/@tests/aqua/src/index.ts @@ -1,5 +1,6 @@ import { fromByteArray } from 'base64-js'; import { Fluence } from '@fluencelabs/js-client.api'; +import type { ClientConfig } from '@fluencelabs/js-client.api'; import { kras, randomKras } from '@fluencelabs/fluence-network-environment'; import { registerHelloWorld, helloTest, marineTest, resourceTest } from './_aqua/smoke_test.js'; import { wasm } from './wasmb64.js'; @@ -11,7 +12,9 @@ import { wasm } from './wasmb64.js'; // }; // Currently the tests executes some calls to registry. And they fail for a single local node setup. So we use kras instead. -const relay = randomKras(); +// TODO DXJ-356: use local peers instead of kras +// const relay = randomKras(); +const relay = kras[4]; function generateRandomUint8Array() { const uint8Array = new Uint8Array(32); @@ -21,7 +24,7 @@ function generateRandomUint8Array() { return uint8Array; } -const optsWithRandomKeyPair = () => { +const optsWithRandomKeyPair = (): ClientConfig => { return { keyPair: { type: 'Ed25519', @@ -37,6 +40,7 @@ export const runTest = async (): Promise => { Fluence.onConnectionStateChange((state) => console.info('connection state changed: ', state)); console.log('connecting to Fluence Network...'); + console.log('multiaddr: ', relay.multiaddr); await Fluence.connect(relay, optsWithRandomKeyPair()); console.log('connected'); @@ -68,10 +72,9 @@ export const runTest = async (): Promise => { const hello = await helloTest(); console.log('hello test finished, result: ', hello); - // TODO: some wired error shit about SharedArrayBuffer - // console.log('running marine test...'); - // const marine = await marineTest(wasm); - // console.log('marine test finished, result: ', marine); + console.log('running marine test...'); + const marine = await marineTest(wasm); + console.log('marine test finished, result: ', marine); const returnVal = { res, @@ -79,8 +82,6 @@ export const runTest = async (): Promise => { // marine, }; return { type: 'success', data: JSON.stringify(returnVal) }; - } catch (err: any) { - return { type: 'failure', error: err.toString() }; } finally { console.log('disconnecting from Fluence Network...'); await Fluence.disconnect(); diff --git a/packages/@tests/marine/node/.gitignore b/packages/@tests/marine/node/.gitignore deleted file mode 100644 index 1436c502..00000000 --- a/packages/@tests/marine/node/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release -bundle/ -/dist/ -/worker/dist/ - -# Dependency directories -node_modules/ -jspm_packages/ - -.idea diff --git a/packages/@tests/marine/node/jest.config.js b/packages/@tests/marine/node/jest.config.js deleted file mode 100644 index 64e531b4..00000000 --- a/packages/@tests/marine/node/jest.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testPathIgnorePatterns: ['dist'], -}; diff --git a/packages/@tests/marine/node/package.json.skip b/packages/@tests/marine/node/package.json.skip deleted file mode 100644 index 2f22d990..00000000 --- a/packages/@tests/marine/node/package.json.skip +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@test/marine_node", - "scripts": { - "build": "tsc", - "test": "jest" - }, - "devDependencies": { - "@types/node": "16.11.59", - "typescript": "^4.0.0", - "@types/jest": "28.1.0", - "jest": "28.1.0", - "ts-jest": "28.0.2" - }, - "dependencies": { - "@fluencelabs/avm": "0.32.1", - "@fluencelabs/marine.background-runner": "0.1.0", - "@fluencelabs/marine.deps-loader.node": "0.1.0" - } -} diff --git a/packages/@tests/marine/node/src/test.spec.ts b/packages/@tests/marine/node/src/test.spec.ts deleted file mode 100644 index f8018a43..00000000 --- a/packages/@tests/marine/node/src/test.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { MarineBackgroundRunner } from '@fluencelabs/marine.background-runner'; -import { InlinedWorkerLoader, WasmNpmLoader } from '@fluencelabs/marine.deps-loader.node'; -import { callAvm, JSONArray, JSONObject } from '@fluencelabs/avm'; - -const vmPeerId = '12D3KooWNzutuy8WHXDKFqFsATvCR6j9cj2FijYbnd47geRKaQZS'; - -describe('Nodejs integration tests', () => { - it('Smoke test', async () => { - let runner: MarineBackgroundRunner | undefined = undefined; - try { - // arrange - const avm = new WasmNpmLoader('@fluencelabs/avm', 'avm.wasm'); - const control = new WasmNpmLoader('@fluencelabs/marine-js', 'marine-js.wasm'); - const worker = new InlinedWorkerLoader(); - runner = new MarineBackgroundRunner(worker, control, () => {}); - - await avm.start(); - - await runner.start(); - await runner.createService(avm.getValue(), 'avm'); - - const s = `(seq - (par - (call "${vmPeerId}" ("local_service_id" "local_fn_name") [] result_1) - (call "remote_peer_id" ("service_id" "fn_name") [] g) - ) - (call "${vmPeerId}" ("local_service_id" "local_fn_name") [] result_2) - )`; - - // act - const res = await callAvm( - (args: JSONArray | JSONObject) => runner!.callService('avm', 'invoke', args, undefined), - { - currentPeerId: vmPeerId, - initPeerId: vmPeerId, - timestamp: Date.now(), - ttl: 10000, - }, - s, - Buffer.from(''), - Buffer.from(''), - [], - ); - - // assert - expect(res).toMatchObject({ - retCode: 0, - errorMessage: '', - }); - } finally { - runner?.stop(); - } - }); -}); diff --git a/packages/@tests/marine/node/tsconfig.json b/packages/@tests/marine/node/tsconfig.json deleted file mode 100644 index 32d340ac..00000000 --- a/packages/@tests/marine/node/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist" - }, - "exclude": ["node_modules", "dist"] -} diff --git a/packages/@tests/marine/web/.gitignore b/packages/@tests/marine/web/.gitignore deleted file mode 100644 index 8aadd61a..00000000 --- a/packages/@tests/marine/web/.gitignore +++ /dev/null @@ -1,22 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -build/ -public/*.* - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release -bundle/ -/dist/ -/worker/dist/ - -# Dependency directories -node_modules/ -jspm_packages/ - -.idea diff --git a/packages/@tests/marine/web/index.html b/packages/@tests/marine/web/index.html deleted file mode 100644 index e3df3163..00000000 --- a/packages/@tests/marine/web/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Webpack App - - -

Hello world!

-

Tip: Check your console

- - - diff --git a/packages/@tests/marine/web/jest.config.js b/packages/@tests/marine/web/jest.config.js deleted file mode 100644 index 22cf0c09..00000000 --- a/packages/@tests/marine/web/jest.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - preset: 'jest-puppeteer', - testMatch: ['**/?(*.)+(spec|test).[t]s'], - testPathIgnorePatterns: ['/node_modules/', 'dist'], - testMatch: ['**/test/*.spec.ts'], - transform: { - '^.+\\.ts?$': 'ts-jest', - }, -}; diff --git a/packages/@tests/marine/web/package.json.skip b/packages/@tests/marine/web/package.json.skip deleted file mode 100644 index de53af98..00000000 --- a/packages/@tests/marine/web/package.json.skip +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@test/marine_web", - "version": "0.1.0", - "scripts": { - "start": "webpack serve", - "test": "jest", - "build": "webpack --mode=production --node-env=production", - "build:dev": "webpack --mode=development", - "build:prod": "webpack --mode=production --node-env=production", - "watch": "webpack --watch", - "serve": "webpack serve" - }, - "devDependencies": { - "@webpack-cli/generators": "^2.4.1", - "css-loader": "^6.5.1", - "html-webpack-plugin": "^5.5.0", - "install-local": "^3.0.1", - "style-loader": "^3.3.1", - "ts-loader": "^8.3.0", - "typescript": "^4.5.4", - "util": "^0.12.4", - "webpack": "^5.65.0", - "webpack-cli": "^4.9.1", - "webpack-dev-server": "^4.6.0", - "@types/jest": "^27.0.3", - "@types/jest-environment-puppeteer": "^4.4.1", - "@types/puppeteer": "^5.4.4", - "jest": "28.1.0", - "jest-puppeteer": "^6.0.2", - "ts-jest": "28.0.2" - }, - "dependencies": { - "@fluencelabs/avm": "0.34.4", - "js-base64": "^3.7.2", - "buffer": "6.0.3" - } -} diff --git a/packages/@tests/marine/web/src/index.ts b/packages/@tests/marine/web/src/index.ts deleted file mode 100644 index 1a54defd..00000000 --- a/packages/@tests/marine/web/src/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Buffer } from 'buffer'; - -// @ts-ignore -window.Buffer = Buffer; - -import { MarineBackgroundRunner } from '@fluencelabs/marine.background-runner'; -import { InlinedWorkerLoader, WasmWebLoader } from '@fluencelabs/marine.deps-loader.web'; -import { callAvm, JSONArray, JSONObject } from '@fluencelabs/avm'; -import { toUint8Array } from 'js-base64'; - -const vmPeerId = '12D3KooWNzutuy8WHXDKFqFsATvCR6j9cj2FijYbnd47geRKaQZS'; - -const b = (s: string) => { - return toUint8Array(s); -}; - -const main = async () => { - const avm = new WasmWebLoader('avm.wasm'); - const control = new WasmWebLoader('marine-js.wasm'); - const worker = new InlinedWorkerLoader(); - const runner = new MarineBackgroundRunner(worker, control, () => {}); - - await runner.start(); - await avm.start(); - const avmVal = await avm.getValue(); - await runner.createService(avmVal, 'avm'); - - const s = `(seq - (par - (call "${vmPeerId}" ("local_service_id" "local_fn_name") [] result_1) - (call "remote_peer_id" ("service_id" "fn_name") [] g) - ) - (call "${vmPeerId}" ("local_service_id" "local_fn_name") [] result_2) - )`; - - // act - const res = await callAvm( - (args: JSONArray | JSONObject) => runner.callService('avm', 'invoke', args, undefined), - { - currentPeerId: vmPeerId, - initPeerId: vmPeerId, - timestamp: Date.now(), - ttl: 10000, - }, - s, - b(''), - b(''), - [], - ); - await runner.stop(); - - return res; -}; - -// @ts-ignore -window.MAIN = main; diff --git a/packages/@tests/marine/web/test/test.spec.ts b/packages/@tests/marine/web/test/test.spec.ts deleted file mode 100644 index 23bc281e..00000000 --- a/packages/@tests/marine/web/test/test.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -import Webpack from 'webpack'; -import WebpackDevServer from 'webpack-dev-server'; -import webpackConfig from '../webpack.config.js'; -import process from 'process'; -import path from 'path'; -import fs from 'fs'; - -// change directory to the location to the test-project. -// run all the subsequent Webpack scripts in that directory -process.chdir(path.join(__dirname, '..')); - -let server; -const port = 8080; - -jest.setTimeout(10000); - -const startServer = async (modifyConfig?) => { - const loadInBrowserToDebug = false; - // const loadInBrowserToDebug = true; // use this line to debug - - modifyConfig = modifyConfig || ((_) => {}); - - const config: any = webpackConfig(); - modifyConfig(config); - config.devServer.open = loadInBrowserToDebug; - server = await makeServer(config); -}; - -// https://stackoverflow.com/questions/42940550/wait-until-webpack-dev-server-is-ready -function makeServer(config) { - return new Promise((resolve, reject) => { - const compiler = Webpack(config); - - let compiled = false; - let listening = false; - - compiler.hooks.done.tap('tap_name', () => { - // console.log('compiled'); - - if (listening) resolve(server); - else compiled = true; - }); - - const server = new WebpackDevServer(compiler, config.devServer); - - server.listen(port, '0.0.0.0', (err) => { - if (err) return reject(err); - - // console.log('listening'); - - if (compiled) { - resolve(server); - } else { - listening = true; - } - }); - }); -} - -const stopServer = async () => { - console.log('test: stopping server'); - await server.stop(); -}; - -const publicDir = 'public'; - -const copyFile = async (packageName: string, fileName: string) => { - const modulePath = require.resolve(packageName); - const source = path.join(path.dirname(modulePath), fileName); - const dest = path.join(publicDir, fileName); - - return fs.promises.copyFile(source, dest); -}; - -const copyPublicDeps = async () => { - await fs.promises.mkdir(publicDir, { recursive: true }); - return Promise.all([ - copyFile('@fluencelabs/marine-js', 'marine-js.wasm'), - copyFile('@fluencelabs/avm', 'avm.wasm'), - ]); -}; - -const cleanPublicDeps = () => { - return fs.promises.rm(publicDir, { recursive: true, force: true }); -}; - -describe('Browser integration tests', () => { - beforeEach(async () => { - await copyPublicDeps(); - }); - - afterEach(async () => { - await stopServer(); - await cleanPublicDeps(); - }); - - it('Some test', async () => { - console.log('test: starting server...'); - await startServer(); - console.log('test: navigating to page...'); - await page.goto('http://localhost:8080/'); - - console.log('test: running script in browser...'); - const res = await page.evaluate(() => { - // @ts-ignore - return window.MAIN(); - }); - - console.log('test: checking expectations...'); - await expect(res).toMatchObject({ - retCode: 0, - errorMessage: '', - }); - }); -}); diff --git a/packages/@tests/marine/web/tsconfig.json b/packages/@tests/marine/web/tsconfig.json deleted file mode 100644 index 32d340ac..00000000 --- a/packages/@tests/marine/web/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist" - }, - "exclude": ["node_modules", "dist"] -} diff --git a/packages/@tests/marine/web/webpack.config.js b/packages/@tests/marine/web/webpack.config.js deleted file mode 100644 index c294c0f9..00000000 --- a/packages/@tests/marine/web/webpack.config.js +++ /dev/null @@ -1,65 +0,0 @@ -// Generated using webpack-cli https://github.com/webpack/webpack-cli - -const path = require('path'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); - -const isProduction = process.env.NODE_ENV == 'production'; - -const stylesHandler = 'style-loader'; - -const config = { - entry: './src/index.ts', - output: { - path: path.resolve(__dirname, 'dist'), - }, - devServer: { - open: true, - host: 'localhost', - static: { - directory: path.join(__dirname, 'public'), - }, - }, - plugins: [ - new HtmlWebpackPlugin({ - template: 'index.html', - }), - - // Add your plugins here - // Learn more about plugins from https://webpack.js.org/configuration/plugins/ - ], - module: { - rules: [ - { - test: /\.(ts|tsx)$/i, - loader: 'ts-loader', - exclude: ['/node_modules/'], - }, - { - test: /\.css$/i, - use: [stylesHandler, 'css-loader'], - }, - { - test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i, - type: 'asset', - }, - - // Add your rules for custom modules here - // Learn more about loaders from https://webpack.js.org/loaders/ - ], - }, - resolve: { - extensions: ['.tsx', '.ts', '.js'], - fallback: { - buffer: require.resolve('buffer/'), - }, - }, -}; - -module.exports = () => { - if (isProduction) { - config.mode = 'production'; - } else { - config.mode = 'development'; - } - return config; -}; diff --git a/packages/@tests/smoke/node/package.json b/packages/@tests/smoke/node/package.json index 93a08ad0..88bf67a4 100644 --- a/packages/@tests/smoke/node/package.json +++ b/packages/@tests/smoke/node/package.json @@ -11,8 +11,7 @@ "type": "module", "scripts": { "build": "tsc", - "test": "node --loader ts-node/esm ./src/index.ts", - "test_logs": "DEBUG=fluence:particle:* node --loader ts-node/esm ./src/index.ts" + "test_commented_out": "node --loader ts-node/esm ./src/index.ts" }, "repository": "https://github.com/fluencelabs/fluence-js", "author": "Fluence Labs", diff --git a/packages/@tests/smoke/web-cra-ts/package.json b/packages/@tests/smoke/web-cra-ts/package.json index d7753528..0f0ca42b 100644 --- a/packages/@tests/smoke/web-cra-ts/package.json +++ b/packages/@tests/smoke/web-cra-ts/package.json @@ -24,6 +24,7 @@ }, "scripts": { "test_commented_out": "node --loader ts-node/esm ./test/index.ts", + "simulate-cdn": "http-server -p 8766 ../../../client/js-client.web.standalone/dist", "start": "react-scripts start", "build": "react-scripts build", "_test": "react-scripts test", diff --git a/packages/@tests/smoke/web/package.json b/packages/@tests/smoke/web/package.json index 7a3602d1..e444b140 100644 --- a/packages/@tests/smoke/web/package.json +++ b/packages/@tests/smoke/web/package.json @@ -11,6 +11,7 @@ "type": "module", "scripts": { "build": "tsc", + "simulate-cdn": "http-server -p 8765 ../../../client/js-client.web.standalone/dist", "test_commented_out": "node --loader ts-node/esm ./src/index.ts", "serve": "http-server public" }, diff --git a/packages/client/api/src/compilerSupport/implementation.ts b/packages/client/api/src/compilerSupport/implementation.ts index d9442f18..ecb4d7ec 100644 --- a/packages/client/api/src/compilerSupport/implementation.ts +++ b/packages/client/api/src/compilerSupport/implementation.ts @@ -29,13 +29,12 @@ import { getFluenceInterface } from '../util.js'; * @param def - function definition generated by the Aqua compiler * @param script - air script with function execution logic generated by the Aqua compiler */ -export const callFunction = async (rawFnArgs: Array, def: FunctionCallDef, script: string): Promise => { +export const v5_callFunction = async ( + rawFnArgs: Array, + def: FunctionCallDef, + script: string, +): Promise => { const { args, client: peer, config } = await extractFunctionArgs(rawFnArgs, def); - if (peer.internals.getConnectionState() !== 'connected') { - throw new Error( - 'Could not call the Aqua function because client is disconnected. Did you forget to call Fluence.connect()?', - ); - } const fluence = await getFluenceInterface(); return fluence.callAquaFunction({ @@ -53,17 +52,9 @@ export const callFunction = async (rawFnArgs: Array, def: FunctionCallDef, * @param args - raw arguments passed by user to the generated function * @param def - service definition generated by the Aqua compiler */ -export const registerService = async (args: any[], def: ServiceDef): Promise => { +export const v5_registerService = async (args: any[], def: ServiceDef): Promise => { const { peer, service, serviceId } = await extractServiceArgs(args, def.defaultServiceId); - // TODO: TBH service registration is just putting some stuff into a hashmap - // there should not be such a check at all - if (peer.internals.getConnectionState() !== 'connected') { - throw new Error( - 'Could not register Aqua service because the client is disconnected. Did you forget to call Fluence.connect()?', - ); - } - const fluence = await getFluenceInterface(); return fluence.registerService({ def, @@ -104,6 +95,11 @@ const extractFunctionArgs = async ( config = args[numberOfExpectedArgs + 1]; } else { const fluence = await getFluenceInterface(); + if (!fluence.defaultClient) { + throw new Error( + 'Could not register Aqua service because the client is not initialized. Did you forget to call Fluence.connect()?', + ); + } peer = fluence.defaultClient; structuredArgs = args.slice(0, numberOfExpectedArgs); config = args[numberOfExpectedArgs]; @@ -145,6 +141,11 @@ const extractServiceArgs = async ( peer = args[0]; } else { const fluence = await getFluenceInterface(); + if (!fluence.defaultClient) { + throw new Error( + 'Could not register Aqua service because the client is not initialized. Did you forget to call Fluence.connect()?', + ); + } peer = fluence.defaultClient; } diff --git a/packages/client/api/src/index.ts b/packages/client/api/src/index.ts index c2cd2152..315ed0bd 100644 --- a/packages/client/api/src/index.ts +++ b/packages/client/api/src/index.ts @@ -1,12 +1,28 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import { getFluenceInterface, getFluenceInterfaceFromGlobalThis } from './util.js'; import { IFluenceClient, - ClientOptions, + ClientConfig, RelayOptions, ConnectionState, - ConnectionStates, + CallAquaFunctionType, + RegisterServiceType, } from '@fluencelabs/interfaces'; -export type { IFluenceClient, ClientOptions, CallParams } from '@fluencelabs/interfaces'; +export type { IFluenceClient, ClientConfig, CallParams } from '@fluencelabs/interfaces'; export { ArrayType, @@ -14,7 +30,6 @@ export { ArrowWithCallbacks, ArrowWithoutCallbacks, BottomType, - FnConfig, FunctionCallConstants, FunctionCallDef, LabeledProductType, @@ -28,12 +43,15 @@ export { StructType, TopType, UnlabeledProductType, + CallAquaFunctionType, + CallAquaFunctionArgs, + PassedArgs, + FnConfig, + RegisterServiceType, + RegisterServiceArgs, } from '@fluencelabs/interfaces'; -export { - callFunction as v5_callFunction, - registerService as v5_registerService, -} from './compilerSupport/implementation.js'; +export { v5_callFunction, v5_registerService } from './compilerSupport/implementation.js'; /** * Public interface to Fluence Network @@ -42,11 +60,12 @@ export const Fluence = { /** * Connect to the Fluence network * @param relay - relay node to connect to - * @param options - client options + * @param config - client configuration */ - connect: async (relay: RelayOptions, options?: ClientOptions): Promise => { + connect: async (relay: RelayOptions, config?: ClientConfig): Promise => { const fluence = await getFluenceInterface(); - return fluence.defaultClient.connect(relay, options); + const client = await fluence.clientFactory(relay, config); + fluence.defaultClient = client; }, /** @@ -54,7 +73,8 @@ export const Fluence = { */ disconnect: async (): Promise => { const fluence = await getFluenceInterface(); - return fluence.defaultClient.disconnect(); + await fluence.defaultClient?.disconnect(); + fluence.defaultClient = undefined; }, /** @@ -62,11 +82,13 @@ export const Fluence = { */ onConnectionStateChange(handler: (state: ConnectionState) => void): ConnectionState { const optimisticResult = getFluenceInterfaceFromGlobalThis(); - if (optimisticResult) { + if (optimisticResult && optimisticResult.defaultClient) { return optimisticResult.defaultClient.onConnectionStateChange(handler); } - getFluenceInterface().then((fluence) => fluence.defaultClient.onConnectionStateChange(handler)); + getFluenceInterface().then((fluence) => { + fluence.defaultClient?.onConnectionStateChange(handler); + }); return 'disconnected'; }, @@ -77,6 +99,9 @@ export const Fluence = { */ getClient: async (): Promise => { const fluence = await getFluenceInterface(); + if (!fluence.defaultClient) { + throw new Error('Fluence client is not initialized. Call Fluence.connect() first'); + } return fluence.defaultClient; }, }; @@ -85,7 +110,23 @@ export const Fluence = { * Low level API. Generally you need Fluence.connect() instead. * @returns IFluenceClient instance */ -export const createClient = async (): Promise => { +export const createClient = async (relay: RelayOptions, config?: ClientConfig): Promise => { const fluence = await getFluenceInterface(); - return fluence.clientFactory(); + return await fluence.clientFactory(relay, config); +}; + +/** + * Low level API. Generally you should use code generated by the Aqua compiler. + */ +export const callAquaFunction: CallAquaFunctionType = async (args) => { + const fluence = await getFluenceInterface(); + return await fluence.callAquaFunction(args); +}; + +/** + * Low level API. Generally you should use code generated by the Aqua compiler. + */ +export const registerService: RegisterServiceType = async (args) => { + const fluence = await getFluenceInterface(); + return await fluence.registerService(args); }; diff --git a/packages/client/api/src/util.ts b/packages/client/api/src/util.ts index d81498f0..4a758be6 100644 --- a/packages/client/api/src/util.ts +++ b/packages/client/api/src/util.ts @@ -1,10 +1,31 @@ -import type { CallAquaFunction, IFluenceClient, RegisterService } from '@fluencelabs/interfaces'; +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { + CallAquaFunctionType, + ClientConfig, + IFluenceClient, + RegisterServiceType, + RelayOptions, +} from '@fluencelabs/interfaces'; type PublicFluenceInterface = { - clientFactory: () => IFluenceClient; - defaultClient: IFluenceClient; - callAquaFunction: CallAquaFunction; - registerService: RegisterService; + defaultClient: IFluenceClient | undefined; + clientFactory: (relay: RelayOptions, config?: ClientConfig) => Promise; + callAquaFunction: CallAquaFunctionType; + registerService: RegisterServiceType; }; export const getFluenceInterfaceFromGlobalThis = (): PublicFluenceInterface | undefined => { diff --git a/packages/client/js-client.node/package.json b/packages/client/js-client.node/package.json index e91180f7..c65bc75f 100644 --- a/packages/client/js-client.node/package.json +++ b/packages/client/js-client.node/package.json @@ -1,33 +1,34 @@ { - "name": "@fluencelabs/js-client.node", - "version": "0.6.7", - "description": "TypeScript implementation of Fluence Peer", - "main": "./dist/index.js", - "typings": "./dist/index.d.ts", - "engines": { - "node": ">=10", - "pnpm": ">=3" - }, - "exports": { - ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" + "name": "@fluencelabs/js-client.node", + "version": "0.6.7", + "description": "TypeScript implementation of Fluence Peer", + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "engines": { + "node": ">=10", + "pnpm": ">=3" + }, + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "type": "module", + "scripts": { + "build": "tsc" + }, + "repository": "https://github.com/fluencelabs/fluence-js", + "author": "Fluence Labs", + "license": "Apache-2.0", + "dependencies": { + "@fluencelabs/js-peer": "0.8.6", + "@fluencelabs/interfaces": "0.7.4", + "@fluencelabs/avm": "0.35.4", + "@fluencelabs/marine-js": "0.3.45", + "platform": "1.3.6" + }, + "devDependencies": { + "@types/platform": "1.3.4" } - }, - "type": "module", - "scripts": { - "build": "tsc" - }, - "repository": "https://github.com/fluencelabs/fluence-js", - "author": "Fluence Labs", - "license": "Apache-2.0", - "dependencies": { - "@fluencelabs/js-peer": "0.8.6", - "@fluencelabs/avm": "0.35.4", - "@fluencelabs/marine-js": "0.3.45", - "platform": "1.3.6" - }, - "devDependencies": { - "@types/platform": "1.3.4" - } } diff --git a/packages/client/js-client.node/src/index.ts b/packages/client/js-client.node/src/index.ts index 7e200bbe..c84d6d14 100644 --- a/packages/client/js-client.node/src/index.ts +++ b/packages/client/js-client.node/src/index.ts @@ -1,12 +1,29 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import * as platform from 'platform'; -import { FluencePeer } from '@fluencelabs/js-peer/dist/js-peer/FluencePeer.js'; +import type { RelayOptions, ClientConfig, IFluenceClient } from '@fluencelabs/interfaces'; +import { ClientPeer, makeClientPeerConfig } from '@fluencelabs/js-peer/dist/clientPeer/ClientPeer.js'; import { callAquaFunction } from '@fluencelabs/js-peer/dist/compilerSupport/callFunction.js'; import { registerService } from '@fluencelabs/js-peer/dist/compilerSupport/registerService.js'; -import { MarineBasedAvmRunner } from '@fluencelabs/js-peer/dist/js-peer/avm.js'; +import { MarineBasedAvmRunner } from '@fluencelabs/js-peer/dist/jsPeer/avm.js'; import { MarineBackgroundRunner } from '@fluencelabs/js-peer/dist/marine/worker/index.js'; import { WasmLoaderFromNpm } from '@fluencelabs/js-peer/dist/marine/deps-loader/node.js'; import { WorkerLoader } from '@fluencelabs/js-peer/dist/marine/worker-script/workerLoader.js'; +import { doRegisterNodeUtils } from '@fluencelabs/js-peer/dist/services/NodeUtils.js'; throwIfNotSupported(); @@ -21,19 +38,26 @@ export const defaultNames = { }, }; -export const createClient = () => { +const createClient = async (relay: RelayOptions, config: ClientConfig): Promise => { const workerLoader = new WorkerLoader(); const controlModuleLoader = new WasmLoaderFromNpm(defaultNames.marine.package, defaultNames.marine.file); const avmModuleLoader = new WasmLoaderFromNpm(defaultNames.avm.package, defaultNames.avm.file); const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader); const avm = new MarineBasedAvmRunner(marine, avmModuleLoader); - return new FluencePeer(marine, avm); + const { keyPair, peerConfig, relayConfig } = await makeClientPeerConfig(relay, config); + const client: IFluenceClient = new ClientPeer(peerConfig, relayConfig, keyPair, marine, avm); + registerNodeOnlyServices(client); + await client.connect(); + return client; }; +function registerNodeOnlyServices(client: IFluenceClient) { + doRegisterNodeUtils(client); +} + const publicFluenceInterface = { clientFactory: createClient, - defaultClient: createClient(), callAquaFunction, registerService, }; diff --git a/packages/client/js-client.web.standalone/package.json b/packages/client/js-client.web.standalone/package.json index c33c4f78..23358692 100644 --- a/packages/client/js-client.web.standalone/package.json +++ b/packages/client/js-client.web.standalone/package.json @@ -1,36 +1,37 @@ { - "name": "@fluencelabs/js-client.web.standalone", - "version": "0.13.6", - "description": "TypeScript implementation of Fluence Peer", - "main": "./dist/index.js", - "typings": "./dist/index.d.ts", - "engines": { - "node": ">=10", - "pnpm": ">=3" - }, - "type": "module", - "scripts": { - "build": "node --loader ts-node/esm ./build.ts" - }, - "repository": "https://github.com/fluencelabs/fluence-js", - "author": "Fluence Labs", - "license": "Apache-2.0", - "dependencies": { - "@fluencelabs/js-peer": "0.8.6", - "buffer": "6.0.3", - "process": "0.11.10" - }, - "devDependencies": { - "@fluencelabs/avm": "0.35.4", - "@fluencelabs/marine-js": "0.3.45", - "@types/node": "16.11.59", - "@types/jest": "28.1.0", - "jest": "28.1.0", - "ts-jest": "28.0.2", - "js-base64": "3.7.5", - "@rollup/plugin-inject": "5.0.3", - "vite-plugin-replace": "0.1.1", - "vite": "4.0.4", - "vite-tsconfig-paths": "4.0.3" - } + "name": "@fluencelabs/js-client.web.standalone", + "version": "0.13.6", + "description": "TypeScript implementation of Fluence Peer", + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "engines": { + "node": ">=10", + "pnpm": ">=3" + }, + "type": "module", + "scripts": { + "build": "node --loader ts-node/esm ./build.ts" + }, + "repository": "https://github.com/fluencelabs/fluence-js", + "author": "Fluence Labs", + "license": "Apache-2.0", + "dependencies": { + "@fluencelabs/js-peer": "0.8.6", + "@fluencelabs/interfaces": "0.7.4", + "buffer": "6.0.3", + "process": "0.11.10" + }, + "devDependencies": { + "@fluencelabs/avm": "0.35.4", + "@fluencelabs/marine-js": "0.3.45", + "@types/node": "16.11.59", + "@types/jest": "28.1.0", + "jest": "28.1.0", + "ts-jest": "28.0.2", + "js-base64": "3.7.5", + "@rollup/plugin-inject": "5.0.3", + "vite-plugin-replace": "0.1.1", + "vite": "4.0.4", + "vite-tsconfig-paths": "4.0.3" + } } diff --git a/packages/client/js-client.web.standalone/src/index.ts b/packages/client/js-client.web.standalone/src/index.ts index 0ecd3a2a..92058654 100644 --- a/packages/client/js-client.web.standalone/src/index.ts +++ b/packages/client/js-client.web.standalone/src/index.ts @@ -1,23 +1,41 @@ -import { FluencePeer } from '@fluencelabs/js-peer/dist/js-peer/FluencePeer.js'; +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { RelayOptions, ClientConfig, IFluenceClient } from '@fluencelabs/interfaces'; +import { ClientPeer, makeClientPeerConfig } from '@fluencelabs/js-peer/dist/clientPeer/ClientPeer.js'; import { callAquaFunction } from '@fluencelabs/js-peer/dist/compilerSupport/callFunction.js'; import { registerService } from '@fluencelabs/js-peer/dist/compilerSupport/registerService.js'; -import { MarineBasedAvmRunner } from '@fluencelabs/js-peer/dist/js-peer/avm.js'; +import { MarineBasedAvmRunner } from '@fluencelabs/js-peer/dist/jsPeer/avm.js'; import { MarineBackgroundRunner } from '@fluencelabs/js-peer/dist/marine/worker'; import { InlinedWorkerLoader, InlinedWasmLoader } from '@fluencelabs/js-peer/dist/marine/deps-loader/common.js'; -const createClient = () => { +const createClient = async (relay: RelayOptions, config: ClientConfig): Promise => { const workerLoader = new InlinedWorkerLoader('___worker___'); const controlModuleLoader = new InlinedWasmLoader('___marine___'); const avmModuleLoader = new InlinedWasmLoader('___avm___'); const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader); const avm = new MarineBasedAvmRunner(marine, avmModuleLoader); - return new FluencePeer(marine, avm); + const { keyPair, peerConfig, relayConfig } = await makeClientPeerConfig(relay, config); + const client: IFluenceClient = new ClientPeer(peerConfig, relayConfig, keyPair, marine, avm); + await client.connect(); + return client; }; const publicFluenceInterface = { clientFactory: createClient, - defaultClient: createClient(), callAquaFunction, registerService, }; diff --git a/packages/client/js-client.web/package.json.skip b/packages/client/js-client.web/package.json similarity index 85% rename from packages/client/js-client.web/package.json.skip rename to packages/client/js-client.web/package.json index 9eb78480..776590cd 100644 --- a/packages/client/js-client.web/package.json.skip +++ b/packages/client/js-client.web/package.json @@ -8,6 +8,7 @@ "node": ">=10", "pnpm": ">=3" }, + "type": "module", "scripts": { "build": "tsc" }, @@ -15,7 +16,8 @@ "author": "Fluence Labs", "license": "Apache-2.0", "dependencies": { - "@fluencelabs/js-peer": "0.7.0" + "@fluencelabs/js-peer": "0.8.6", + "@fluencelabs/interfaces": "0.7.4" }, "devDependencies": { "@types/node": "16.11.59", diff --git a/packages/client/js-client.web/src/index.ts b/packages/client/js-client.web/src/index.ts index 1b74bdf5..946467ce 100644 --- a/packages/client/js-client.web/src/index.ts +++ b/packages/client/js-client.web/src/index.ts @@ -1,23 +1,50 @@ -import { MarineBackgroundRunner } from '@fluencelabs/marine.background-runner'; -import { MarineBasedAvmRunner } from '@fluencelabs/js-peer/dist/avm'; -import { marineLogFunction } from '@fluencelabs/js-peer/dist/utils'; -import { FluencePeer } from '@fluencelabs/js-peer/dist/FluencePeer'; -import { InlinedWorkerLoader, WasmWebLoader } from '@fluencelabs/marine.deps-loader.web'; +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { RelayOptions, ClientConfig, IFluenceClient } from '@fluencelabs/interfaces'; +import { ClientPeer, makeClientPeerConfig } from '@fluencelabs/js-peer/dist/clientPeer/ClientPeer.js'; +import { callAquaFunction } from '@fluencelabs/js-peer/dist/compilerSupport/callFunction.js'; +import { registerService } from '@fluencelabs/js-peer/dist/compilerSupport/registerService.js'; +import { MarineBasedAvmRunner } from '@fluencelabs/js-peer/dist/jsPeer/avm.js'; +import { MarineBackgroundRunner } from '@fluencelabs/js-peer/dist/marine/worker'; +import { WasmLoaderFromUrl, WorkerLoaderFromUrl } from '@fluencelabs/js-peer/dist/marine/deps-loader/web.js'; -export const defaultNames = { - avm: 'avm.wasm', +const defaultNames = { marine: 'marine-js.wasm', + avm: 'avm.wasm', + worker: 'worker-script.js', }; -export const makeDefaultPeer = () => { - const workerLoader = new InlinedWorkerLoader(); - const controlModuleLoader = new WasmWebLoader(defaultNames.marine); - const avmModuleLoader = new WasmWebLoader(defaultNames.avm); +const createClient = async (relay: RelayOptions, config: ClientConfig): Promise => { + const workerLoader = new WorkerLoaderFromUrl(defaultNames.worker); + const controlModuleLoader = new WasmLoaderFromUrl(defaultNames.marine); + const avmModuleLoader = new WasmLoaderFromUrl(defaultNames.avm); - const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader, marineLogFunction); - const avm = new MarineBasedAvmRunner(marine, avmModuleLoader, undefined); - return new FluencePeer(marine, avm); + const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader); + const avm = new MarineBasedAvmRunner(marine, avmModuleLoader); + const { keyPair, peerConfig, relayConfig } = await makeClientPeerConfig(relay, config); + const client: IFluenceClient = new ClientPeer(peerConfig, relayConfig, keyPair, marine, avm); + await client.connect(); + return client; +}; + +const publicFluenceInterface = { + clientFactory: createClient, + callAquaFunction, + registerService, }; // @ts-ignore -globalThis.defaultPeer = makeDefaultPeer(); +globalThis.fluence = publicFluenceInterface; diff --git a/packages/core/interfaces/src/commonTypes.ts b/packages/core/interfaces/src/commonTypes.ts new file mode 100644 index 00000000..d7e64f5a --- /dev/null +++ b/packages/core/interfaces/src/commonTypes.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { SecurityTetraplet } from '@fluencelabs/avm'; + +/** + * Peer ID's id as a base58 string (multihash/CIDv0). + */ +export type PeerIdB58 = string; + +/** + * Node of the Fluence network specified as a pair of node's multiaddr and it's peer id + */ +export type Node = { + peerId: PeerIdB58; + multiaddr: string; +}; + +/** + * Additional information about a service call + * @typeparam ArgName + */ +export interface CallParams { + /** + * The identifier of particle which triggered the call + */ + particleId: string; + + /** + * The peer id which created the particle + */ + initPeerId: PeerIdB58; + + /** + * Particle's timestamp when it was created + */ + timestamp: number; + + /** + * Time to live in milliseconds. The time after the particle should be expired + */ + ttl: number; + + /** + * Particle's signature + */ + signature?: string; + + /** + * Security tetraplets + */ + tetraplets: ArgName extends string ? Record : Record; +} diff --git a/packages/core/interfaces/src/compilerSupport.ts b/packages/core/interfaces/src/compilerSupport/aquaTypeDefinitions.ts similarity index 90% rename from packages/core/interfaces/src/compilerSupport.ts rename to packages/core/interfaces/src/compilerSupport/aquaTypeDefinitions.ts index 01b8361a..fa7822fc 100644 --- a/packages/core/interfaces/src/compilerSupport.ts +++ b/packages/core/interfaces/src/compilerSupport/aquaTypeDefinitions.ts @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ type SomeNonArrowTypes = ScalarType | OptionType | ArrayType | StructType | TopType | BottomType; export type NonArrowType = SomeNonArrowTypes | ProductType; diff --git a/packages/core/interfaces/src/compilerSupport/compilerSupportInterface.ts b/packages/core/interfaces/src/compilerSupport/compilerSupportInterface.ts new file mode 100644 index 00000000..515ab90a --- /dev/null +++ b/packages/core/interfaces/src/compilerSupport/compilerSupportInterface.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { IFluenceInternalApi } from '../fluenceClient.js'; +import { FnConfig, FunctionCallDef, ServiceDef } from './aquaTypeDefinitions.js'; + +/** + * Arguments passed to Aqua function + */ +export type PassedArgs = { [key: string]: any }; + +/** + * Arguments for callAquaFunction function + */ +export interface CallAquaFunctionArgs { + /** + * Peer to call the function on + */ + peer: IFluenceInternalApi; + + /** + * Function definition + */ + def: FunctionCallDef; + + /** + * Air script used by the aqua function + */ + script: string; + + /** + * Function configuration + */ + config: FnConfig; + + /** + * Arguments to pass to the function + */ + args: PassedArgs; +} + +/** + * Call a function from Aqua script + */ +export type CallAquaFunctionType = (args: CallAquaFunctionArgs) => Promise; + +/** + * Arguments for registerService function + */ +export interface RegisterServiceArgs { + /** + * Peer to register the service on + */ + peer: IFluenceInternalApi; + + /** + * Service definition + */ + def: ServiceDef; + + /** + * Service id + */ + serviceId: string | undefined; + + /** + * Service implementation + */ + service: any; +} + +/** + * Register a service defined in Aqua on a Fluence peer + */ +export type RegisterServiceType = (args: RegisterServiceArgs) => void; diff --git a/packages/core/interfaces/src/fluenceClient.ts b/packages/core/interfaces/src/fluenceClient.ts index 7b67c5fa..314cf55f 100644 --- a/packages/core/interfaces/src/fluenceClient.ts +++ b/packages/core/interfaces/src/fluenceClient.ts @@ -1,70 +1,36 @@ -import type { SecurityTetraplet } from '@fluencelabs/avm'; -import type { LogLevel } from '@fluencelabs/marine-js/dist/types'; -// import type { MultiaddrInput } from '@multiformats/multiaddr'; -import type { FnConfig, FunctionCallDef, ServiceDef } from './compilerSupport.js'; - -/** - * Peer ID's id as a base58 string (multihash/CIDv0). +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -export type PeerIdB58 = string; - -/** - * Additional information about a service call - * @typeparam ArgName - */ -export interface CallParams { - /** - * The identifier of particle which triggered the call - */ - particleId: string; - - /** - * The peer id which created the particle - */ - initPeerId: PeerIdB58; - - /** - * Particle's timestamp when it was created - */ - timestamp: number; - - /** - * Time to live in milliseconds. The time after the particle should be expired - */ - ttl: number; - - /** - * Particle's signature - */ - signature?: string; - - /** - * Security tetraplets - */ - tetraplets: ArgName extends string ? Record : Record; -} - -/** - * Node of the Fluence network specified as a pair of node's multiaddr and it's peer id - */ -type Node = { - peerId: PeerIdB58; - multiaddr: string; -}; - -// TODO: either drop support for this exact type or get it back +import type { Node } from './commonTypes.js'; /** * A node in Fluence network a client can connect to. * Can be in the form of: * - string: multiaddr in string format - * - Multiaddr: multiaddr object, @see https://github.com/multiformats/js-multiaddr * - Node: node structure, @see Node */ -export type RelayOptions = string | /* MultiaddrInput | */ Node; +export type RelayOptions = string | Node; +/** + * Fluence Peer's key pair types + */ export type KeyTypes = 'RSA' | 'Ed25519' | 'secp256k1'; +/** + * Options to specify key pair used in Fluence Peer + */ export type KeyPairOptions = { type: 'Ed25519'; source: 'random' | Uint8Array; @@ -73,13 +39,16 @@ export type KeyPairOptions = { /** * Configuration used when initiating Fluence Client */ -export interface ClientOptions { +export interface ClientConfig { /** * Specify the KeyPair to be used to identify the Fluence Peer. * Will be generated randomly if not specified */ keyPair?: KeyPairOptions; + /** + * Options to configure the connection to the Fluence network + */ connectionOptions?: { /** * When the peer established the connection to the network it sends a ping-like message to check if it works correctly. @@ -92,6 +61,18 @@ export interface ClientOptions { * The dialing timeout in milliseconds */ dialTimeoutMs?: number; + + /** + * The maximum number of inbound streams for the libp2p node. + * Default: 1024 + */ + maxInboundStreams?: number; + + /** + * The maximum number of outbound streams for the libp2p node. + * Default: 1024 + */ + maxOutboundStreams?: number; }; /** @@ -113,18 +94,31 @@ export interface ClientOptions { }; } -export type FluenceStartConfig = ClientOptions & { relay: RelayOptions }; - +/** + * Fluence JS Client connection states as string literals + */ export const ConnectionStates = ['disconnected', 'connecting', 'connected', 'disconnecting'] as const; -export type ConnectionState = typeof ConnectionStates[number]; -export interface IFluenceClient { +/** + * Fluence JS Client connection states + */ +export type ConnectionState = (typeof ConnectionStates)[number]; + +export interface IFluenceInternalApi { + /** + * Internal API + */ + internals: any; +} + +/** + * Public API of Fluence JS Client + */ +export interface IFluenceClient extends IFluenceInternalApi { /** * Connect to the Fluence network - * @param relay - relay node to connect to - * @param options - client options */ - connect: (relay: RelayOptions, options?: ClientOptions) => Promise; + connect: () => Promise; /** * Disconnect from the Fluence network @@ -150,36 +144,11 @@ export interface IFluenceClient { * Return relay's public key as a base58 string (multihash/CIDv0). */ getRelayPeerId(): string; - - // TODO: come up with a working interface for - // - particle creation - // - particle initialization - // - service registration - /** - * For internal use only. Do not call directly - */ - internals: any; } -export interface CallAquaFunctionArgs { - def: FunctionCallDef; - script: string; - config: FnConfig; - peer: IFluenceClient; - args: { [key: string]: any }; -} - -export type CallAquaFunction = (args: CallAquaFunctionArgs) => Promise; - -export interface RegisterServiceArgs { - peer: IFluenceClient; - def: ServiceDef; - serviceId: string | undefined; - service: any; -} - -export type RegisterService = (args: RegisterServiceArgs) => void; - +/** + * For internal use. Checks if the object is a Fluence Peer + */ export const asFluencePeer = (fluencePeerCandidate: unknown): IFluenceClient => { if (isFluencePeer(fluencePeerCandidate)) { return fluencePeerCandidate; @@ -188,6 +157,9 @@ export const asFluencePeer = (fluencePeerCandidate: unknown): IFluenceClient => throw new Error(`Argument ${fluencePeerCandidate} is not a Fluence Peer`); }; +/** + * For internal use. Checks if the object is a Fluence Peer + */ export const isFluencePeer = (fluencePeerCandidate: unknown): fluencePeerCandidate is IFluenceClient => { if (fluencePeerCandidate && (fluencePeerCandidate as any).__isFluenceAwesome) { return true; diff --git a/packages/core/interfaces/src/index.ts b/packages/core/interfaces/src/index.ts index e4241e03..f175fab7 100644 --- a/packages/core/interfaces/src/index.ts +++ b/packages/core/interfaces/src/index.ts @@ -1,2 +1,19 @@ -export * from './compilerSupport.js' -export * from './fluenceClient.js' \ No newline at end of file +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './compilerSupport/aquaTypeDefinitions.js'; +export * from './compilerSupport/compilerSupportInterface.js'; +export * from './commonTypes.js'; +export * from './fluenceClient.js'; diff --git a/packages/core/js-peer/src/js-peer/__test__/data/greeting-record.wasm b/packages/core/js-peer/data_for_test/greeting-record.wasm similarity index 100% rename from packages/core/js-peer/src/js-peer/__test__/data/greeting-record.wasm rename to packages/core/js-peer/data_for_test/greeting-record.wasm diff --git a/packages/core/js-peer/src/js-peer/__test__/data/greeting.wasm b/packages/core/js-peer/data_for_test/greeting.wasm similarity index 100% rename from packages/core/js-peer/src/js-peer/__test__/data/greeting.wasm rename to packages/core/js-peer/data_for_test/greeting.wasm diff --git a/packages/core/js-peer/package.json b/packages/core/js-peer/package.json index a47c0512..720dd7e0 100644 --- a/packages/core/js-peer/package.json +++ b/packages/core/js-peer/package.json @@ -12,7 +12,7 @@ "scripts": { "build": "tsc", "compile-aqua": "fluence aqua -i ./aqua/ -o ./aqua", - "test": "node ./copy-worker-script-workaround.mjs && vitest run" + "test": "node ./copy-worker-script-workaround.mjs && vitest --threads false run" }, "repository": "https://github.com/fluencelabs/fluence-js", "author": "Fluence Labs", diff --git a/packages/core/js-peer/src/clientPeer/ClientPeer.ts b/packages/core/js-peer/src/clientPeer/ClientPeer.ts new file mode 100644 index 00000000..20ef08fa --- /dev/null +++ b/packages/core/js-peer/src/clientPeer/ClientPeer.ts @@ -0,0 +1,133 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ClientConfig, ConnectionState, IFluenceClient, PeerIdB58, RelayOptions } from '@fluencelabs/interfaces'; +import { RelayConnection, RelayConnectionConfig } from '../connection/RelayConnection.js'; +import { fromOpts, KeyPair } from '../keypair/index.js'; +import { FluencePeer, PeerConfig } from '../jsPeer/FluencePeer.js'; +import { relayOptionToMultiaddr } from '../util/libp2pUtils.js'; +import { IAvmRunner, IMarineHost } from '../marine/interfaces.js'; +import { JsServiceHost } from '../jsServiceHost/JsServiceHost.js'; +import { logger } from '../util/logger.js'; + +const log = logger('client'); + +const DEFAULT_TTL_MS = 7000; +const MAX_OUTBOUND_STREAMS = 1024; +const MAX_INBOUND_STREAMS = 1024; + +export const makeClientPeerConfig = async ( + relay: RelayOptions, + config: ClientConfig, +): Promise<{ peerConfig: PeerConfig; relayConfig: RelayConnectionConfig; keyPair: KeyPair }> => { + const opts = config?.keyPair || { type: 'Ed25519', source: 'random' }; + const keyPair = await fromOpts(opts); + const relayAddress = relayOptionToMultiaddr(relay); + + return { + peerConfig: { + debug: { + printParticleId: config?.debug?.printParticleId || false, + }, + defaultTtlMs: config?.defaultTtlMs || DEFAULT_TTL_MS, + }, + relayConfig: { + peerId: keyPair.getLibp2pPeerId(), + relayAddress: relayAddress, + dialTimeoutMs: config?.connectionOptions?.dialTimeoutMs, + maxInboundStreams: config?.connectionOptions?.maxInboundStreams || MAX_OUTBOUND_STREAMS, + maxOutboundStreams: config?.connectionOptions?.maxOutboundStreams || MAX_INBOUND_STREAMS, + }, + keyPair: keyPair, + }; +}; + +export class ClientPeer extends FluencePeer implements IFluenceClient { + private relayPeerId: PeerIdB58; + private relayConnection: RelayConnection; + + constructor( + peerConfig: PeerConfig, + relayConfig: RelayConnectionConfig, + keyPair: KeyPair, + marine: IMarineHost, + avmRunner: IAvmRunner, + ) { + const relayConnection = new RelayConnection(relayConfig); + + super(peerConfig, keyPair, marine, new JsServiceHost(), avmRunner, relayConnection); + this.relayPeerId = relayConnection.getRelayPeerId(); + this.relayConnection = relayConnection; + } + + getPeerId(): string { + return this.keyPair.getPeerId(); + } + + getPeerSecretKey(): Uint8Array { + return this.keyPair.toEd25519PrivateKey(); + } + + connectionState: ConnectionState = 'disconnected'; + connectionStateChangeHandler: (state: ConnectionState) => void = () => {}; + + getRelayPeerId(): string { + return this.relayPeerId; + } + + onConnectionStateChange(handler: (state: ConnectionState) => void): ConnectionState { + this.connectionStateChangeHandler = handler; + + return this.connectionState; + } + + private changeConnectionState(state: ConnectionState) { + this.connectionState = state; + this.connectionStateChangeHandler(state); + } + + /** + * Connect to the Fluence network + */ + async connect(): Promise { + return this.start(); + } + + // /** + // * Disconnect from the Fluence network + // */ + async disconnect(): Promise { + return this.stop(); + } + + async start(): Promise { + log.trace('connecting to Fluence network'); + this.changeConnectionState('connecting'); + await super.start(); + await this.relayConnection.start(); + // TODO: check connection (`checkConnection` function) here + this.changeConnectionState('connected'); + log.trace('connected'); + } + + async stop(): Promise { + log.trace('disconnecting from Fluence network'); + this.changeConnectionState('disconnecting'); + await this.relayConnection.stop(); + await super.stop(); + this.changeConnectionState('disconnected'); + log.trace('disconnected'); + } +} diff --git a/packages/core/js-peer/src/clientPeer/__test__/client.spec.ts b/packages/core/js-peer/src/clientPeer/__test__/client.spec.ts new file mode 100644 index 00000000..25e070c3 --- /dev/null +++ b/packages/core/js-peer/src/clientPeer/__test__/client.spec.ts @@ -0,0 +1,187 @@ +import { it, describe, expect } from 'vitest'; +import { handleTimeout } from '../../particle/Particle.js'; +import { doNothing } from '../../jsServiceHost/serviceUtils.js'; +import { registerHandlersHelper, withClient } from '../../util/testUtils.js'; +import { checkConnection } from '../checkConnection.js'; +import { nodes, RELAY } from './connection.js'; +import { CallServiceData } from '../../jsServiceHost/interfaces.js'; + +describe('FluenceClient usage test suite', () => { + it('should make a call through network', async () => { + await withClient(RELAY, {}, async (peer) => { + // arrange + + const result = await new Promise((resolve, reject) => { + const script = ` + (xor + (seq + (call %init_peer_id% ("load" "relay") [] init_relay) + (seq + (call init_relay ("op" "identity") ["hello world!"] result) + (call %init_peer_id% ("callback" "callback") [result]) + ) + ) + (seq + (call init_relay ("op" "identity") []) + (call %init_peer_id% ("callback" "error") [%last_error%]) + ) + )`; + const particle = peer.internals.createNewParticle(script); + + if (particle instanceof Error) { + return reject(particle.message); + } + + registerHandlersHelper(peer, particle, { + load: { + relay: () => { + return peer.getRelayPeerId(); + }, + }, + callback: { + callback: (args: any) => { + const [val] = args; + resolve(val); + }, + error: (args: any) => { + const [error] = args; + reject(error); + }, + }, + }); + + peer.internals.initiateParticle(particle, handleTimeout(reject)); + }); + + expect(result).toBe('hello world!'); + }); + }); + + it('check connection should work', async function () { + await withClient(RELAY, {}, async (peer) => { + const isConnected = await checkConnection(peer); + + expect(isConnected).toEqual(true); + }); + }); + + it('check connection should work with ttl', async function () { + await withClient(RELAY, {}, async (peer) => { + const isConnected = await checkConnection(peer, 10000); + + expect(isConnected).toEqual(true); + }); + }); + + it('two clients should work inside the same time javascript process', async () => { + await withClient(RELAY, {}, async (peer1) => { + await withClient(RELAY, {}, async (peer2) => { + const res = new Promise((resolve) => { + peer2.internals.regHandler.common('test', 'test', (req: CallServiceData) => { + resolve(req.args[0]); + return { + result: {}, + retCode: 0, + }; + }); + }); + + const script = ` + (seq + (call "${peer1.getRelayPeerId()}" ("op" "identity") []) + (call "${peer2.getPeerId()}" ("test" "test") ["test"]) + ) + `; + const particle = peer1.internals.createNewParticle(script); + + if (particle instanceof Error) { + throw particle; + } + + peer1.internals.initiateParticle(particle, doNothing); + + expect(await res).toEqual('test'); + }); + }); + }); + + describe('should make connection to network', () => { + it('address as string', async () => { + await withClient(nodes[0].multiaddr, {}, async (peer) => { + const isConnected = await checkConnection(peer); + + expect(isConnected).toBeTruthy(); + }); + }); + + it('address as node', async () => { + await withClient(nodes[0], {}, async (peer) => { + const isConnected = await checkConnection(peer); + + expect(isConnected).toBeTruthy(); + }); + }); + + it('With connection options: dialTimeout', async () => { + await withClient(RELAY, { connectionOptions: { dialTimeoutMs: 100000 } }, async (peer) => { + const isConnected = await checkConnection(peer); + + expect(isConnected).toBeTruthy(); + }); + }); + + it('With connection options: skipCheckConnection', async () => { + await withClient(RELAY, { connectionOptions: { skipCheckConnection: true } }, async (peer) => { + const isConnected = await checkConnection(peer); + + expect(isConnected).toBeTruthy(); + }); + }); + + it('With connection options: defaultTTL', async () => { + await withClient(RELAY, { defaultTtlMs: 1 }, async (peer) => { + const isConnected = await checkConnection(peer); + + expect(isConnected).toBeFalsy(); + }); + }); + }); + + it.skip('Should throw correct error when the client tries to send a particle not to the relay', async () => { + await withClient(RELAY, {}, async (peer) => { + const promise = new Promise((resolve, reject) => { + const script = ` + (xor + (call "incorrect_peer_id" ("any" "service") []) + (call %init_peer_id% ("callback" "error") [%last_error%]) + )`; + const particle = peer.internals.createNewParticle(script); + + if (particle instanceof Error) { + return reject(particle.message); + } + + registerHandlersHelper(peer, particle, { + callback: { + error: (args: any) => { + const [error] = args; + reject(error); + }, + }, + }); + + peer.internals.initiateParticle(particle, (stage) => { + if (stage.stage === 'sendingError') { + reject(stage.errorMessage); + } + }); + }); + + await promise; + + await expect(promise).rejects.toMatch( + 'Particle is expected to be sent to only the single peer (relay which client is connected to)', + ); + }); + }); +}); diff --git a/packages/core/js-peer/src/js-peer/__test__/connection.ts b/packages/core/js-peer/src/clientPeer/__test__/connection.ts similarity index 93% rename from packages/core/js-peer/src/js-peer/__test__/connection.ts rename to packages/core/js-peer/src/clientPeer/__test__/connection.ts index f3fafa1d..0afb0a12 100644 --- a/packages/core/js-peer/src/js-peer/__test__/connection.ts +++ b/packages/core/js-peer/src/clientPeer/__test__/connection.ts @@ -15,3 +15,5 @@ export const nodes = [ peerId: '12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3', }, ]; + +export const RELAY = nodes[0].multiaddr; diff --git a/packages/core/js-peer/src/js-peer/utils.ts b/packages/core/js-peer/src/clientPeer/checkConnection.ts similarity index 65% rename from packages/core/js-peer/src/js-peer/utils.ts rename to packages/core/js-peer/src/clientPeer/checkConnection.ts index 4b838749..71bfa39a 100644 --- a/packages/core/js-peer/src/js-peer/utils.ts +++ b/packages/core/js-peer/src/clientPeer/checkConnection.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 Fluence Labs Limited + * Copyright 2023 Fluence Labs Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,39 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { Buffer } from 'buffer'; -import { CallServiceData, CallServiceResult, CallServiceResultType, ResultCodes } from '../interfaces/commonTypes.js'; -import { FluencePeer } from './FluencePeer.js'; -import { ParticleExecutionStage } from './Particle.js'; +import { ClientPeer } from './ClientPeer.js'; import { logger } from '../util/logger.js'; +import { WrapFnIntoServiceCall } from '../jsServiceHost/serviceUtils.js'; +import { handleTimeout } from '../particle/Particle.js'; const log = logger('connection'); -export const MakeServiceCall = - (fn: (args: any[]) => CallServiceResultType) => - (req: CallServiceData): CallServiceResult => ({ - retCode: ResultCodes.success, - result: fn(req.args), - }); - -export const handleTimeout = (fn: () => void) => (stage: ParticleExecutionStage) => { - if (stage.stage === 'expired') { - fn(); - } -}; -export const doNothing = (..._args: Array) => undefined; - /** * Checks the network connection by sending a ping-like request to relay node - * @param { FluenceClient } peer - The Fluence Client instance. + * @param { ClientPeer } peer - The Fluence Client instance. */ -export const checkConnection = async (peer: FluencePeer, ttl?: number): Promise => { - if (!peer.getStatus().isConnected) { - return false; - } - +export const checkConnection = async (peer: ClientPeer, ttl?: number): Promise => { const msg = Math.random().toString(36).substring(7); const promise = new Promise((resolve, reject) => { @@ -76,8 +56,8 @@ export const checkConnection = async (peer: FluencePeer, ttl?: number): Promise< particle.id, 'load', 'relay', - MakeServiceCall(() => { - return peer.getStatus().relayPeerId; + WrapFnIntoServiceCall(() => { + return peer.getRelayPeerId(); }), ); @@ -85,7 +65,7 @@ export const checkConnection = async (peer: FluencePeer, ttl?: number): Promise< particle.id, 'load', 'msg', - MakeServiceCall(() => { + WrapFnIntoServiceCall(() => { return msg; }), ); @@ -94,7 +74,7 @@ export const checkConnection = async (peer: FluencePeer, ttl?: number): Promise< particle.id, 'callback', 'callback', - MakeServiceCall((args) => { + WrapFnIntoServiceCall((args) => { const [val] = args; setTimeout(() => { resolve(val); @@ -107,7 +87,7 @@ export const checkConnection = async (peer: FluencePeer, ttl?: number): Promise< particle.id, 'callback', 'error', - MakeServiceCall((args) => { + WrapFnIntoServiceCall((args) => { const [error] = args; setTimeout(() => { reject(error); @@ -131,23 +111,7 @@ export const checkConnection = async (peer: FluencePeer, ttl?: number): Promise< } return true; } catch (e) { - log.error('error on establishing connection. Relay: %s error: %j', e, peer.getStatus().relayPeerId); + log.error('error on establishing connection. Relay: %s error: %j', e, peer.getRelayPeerId()); return false; } }; - -export function jsonify(obj: unknown) { - return JSON.stringify(obj, null, 4); -} - -export const isString = (x: unknown): x is string => { - return x !== null && typeof x === 'string'; -}; - -export class ServiceError extends Error { - constructor(message: string) { - super(message); - - Object.setPrototypeOf(this, ServiceError.prototype); - } -} diff --git a/packages/core/js-peer/src/compilerSupport/callFunction.ts b/packages/core/js-peer/src/compilerSupport/callFunction.ts index 945b6169..9eb58625 100644 --- a/packages/core/js-peer/src/compilerSupport/callFunction.ts +++ b/packages/core/js-peer/src/compilerSupport/callFunction.ts @@ -1,13 +1,19 @@ -import { - ArrowWithoutCallbacks, - FnConfig, - FunctionCallDef, - NonArrowType, - getArgumentTypes, - isReturnTypeVoid, - IFluenceClient, - CallAquaFunction, -} from '@fluencelabs/interfaces'; +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { getArgumentTypes, isReturnTypeVoid, CallAquaFunctionType } from '@fluencelabs/interfaces'; import { injectRelayService, @@ -34,7 +40,7 @@ const log = logger('aqua'); * @param args - args in the form of JSON where each key corresponds to the name of the argument * @returns */ -export const callAquaFunction: CallAquaFunction = ({ def, script, config, peer, args }) => { +export const callAquaFunction: CallAquaFunctionType = ({ def, script, config, peer, args }) => { log.trace('calling aqua function %j', { def, script, config, args }); const argumentTypes = getArgumentTypes(def); diff --git a/packages/core/js-peer/src/compilerSupport/conversions.ts b/packages/core/js-peer/src/compilerSupport/conversions.ts index 08af1ad9..5840313f 100644 --- a/packages/core/js-peer/src/compilerSupport/conversions.ts +++ b/packages/core/js-peer/src/compilerSupport/conversions.ts @@ -1,7 +1,22 @@ -import { jsonify } from '../js-peer/utils.js'; +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { jsonify } from '../util/utils.js'; import { match } from 'ts-pattern'; import type { ArrowType, ArrowWithoutCallbacks, NonArrowType } from '@fluencelabs/interfaces'; -import { CallServiceData } from '../interfaces/commonTypes.js'; +import { CallServiceData } from '../jsServiceHost/interfaces.js'; /** * Convert value from its representation in aqua language to representation in typescript diff --git a/packages/core/js-peer/src/compilerSupport/registerService.ts b/packages/core/js-peer/src/compilerSupport/registerService.ts index a9786c88..3c687b8c 100644 --- a/packages/core/js-peer/src/compilerSupport/registerService.ts +++ b/packages/core/js-peer/src/compilerSupport/registerService.ts @@ -1,11 +1,26 @@ -import type { RegisterService } from '@fluencelabs/interfaces'; +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { RegisterServiceType } from '@fluencelabs/interfaces'; import { registerGlobalService, userHandlerService } from './services.js'; import { logger } from '../util/logger.js'; const log = logger('aqua'); -export const registerService: RegisterService = ({ peer, def, serviceId, service }) => { +export const registerService: RegisterServiceType = ({ peer, def, serviceId, service }) => { log.trace('registering aqua service %o', { def, serviceId, service }); // Checking for missing keys diff --git a/packages/core/js-peer/src/compilerSupport/services.ts b/packages/core/js-peer/src/compilerSupport/services.ts index f71b506d..834bbe7b 100644 --- a/packages/core/js-peer/src/compilerSupport/services.ts +++ b/packages/core/js-peer/src/compilerSupport/services.ts @@ -1,18 +1,33 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import { SecurityTetraplet } from '@fluencelabs/avm'; import { match } from 'ts-pattern'; -import { Particle } from '../js-peer/Particle.js'; -import { CallServiceData, GenericCallServiceHandler, ResultCodes } from '../interfaces/commonTypes.js'; +import { Particle } from '../particle/Particle.js'; import { aquaArgs2Ts, responseServiceValue2ts, returnType2Aqua, ts2aqua } from './conversions.js'; import { - IFluenceClient, CallParams, ArrowWithoutCallbacks, FunctionCallConstants, FunctionCallDef, NonArrowType, + IFluenceInternalApi, } from '@fluencelabs/interfaces'; +import { CallServiceData, GenericCallServiceHandler, ResultCodes } from '../jsServiceHost/interfaces.js'; export interface ServiceDescription { serviceId: string; @@ -23,7 +38,7 @@ export interface ServiceDescription { /** * Creates a service which injects relay's peer id into aqua space */ -export const injectRelayService = (def: FunctionCallDef, peer: IFluenceClient) => { +export const injectRelayService = (def: FunctionCallDef, peer: IFluenceInternalApi) => { return { serviceId: def.names.getDataSrv, fnName: def.names.relay, @@ -168,10 +183,14 @@ const extractCallParams = (req: CallServiceData, arrow: ArrowWithoutCallbacks): return callParams; }; -export const registerParticleScopeService = (peer: IFluenceClient, particle: Particle, service: ServiceDescription) => { +export const registerParticleScopeService = ( + peer: IFluenceInternalApi, + particle: Particle, + service: ServiceDescription, +) => { peer.internals.regHandler.forParticle(particle.id, service.serviceId, service.fnName, service.handler); }; -export const registerGlobalService = (peer: IFluenceClient, service: ServiceDescription) => { +export const registerGlobalService = (peer: IFluenceInternalApi, service: ServiceDescription) => { peer.internals.regHandler.common(service.serviceId, service.fnName, service.handler); }; diff --git a/packages/core/js-peer/src/connection/index.ts b/packages/core/js-peer/src/connection/RelayConnection.ts similarity index 56% rename from packages/core/js-peer/src/connection/index.ts rename to packages/core/js-peer/src/connection/RelayConnection.ts index da601d1e..da2f16b3 100644 --- a/packages/core/js-peer/src/connection/index.ts +++ b/packages/core/js-peer/src/connection/RelayConnection.ts @@ -14,7 +14,6 @@ * limitations under the License. */ import { PeerIdB58 } from '@fluencelabs/interfaces'; -import { FluenceConnection, ParticleHandler } from '../interfaces/index.js'; import { pipe } from 'it-pipe'; import { encode, decode } from 'it-length-prefixed'; import type { PeerId } from '@libp2p/interface-peer-id'; @@ -25,23 +24,28 @@ import { mplex } from '@libp2p/mplex'; import { webSockets } from '@libp2p/websockets'; import { all } from '@libp2p/websockets/filters'; import { multiaddr } from '@multiformats/multiaddr'; -import type { MultiaddrInput, Multiaddr } from '@multiformats/multiaddr'; -import type { Connection } from '@libp2p/interface-connection'; +import type { Multiaddr } from '@multiformats/multiaddr'; import map from 'it-map'; import { fromString } from 'uint8arrays/from-string'; import { toString } from 'uint8arrays/to-string'; import { logger } from '../util/logger.js'; +import { Subject } from 'rxjs'; +import { throwIfHasNoPeerId } from '../util/libp2pUtils.js'; +import { IConnection } from './interfaces.js'; +import { IParticle } from '../particle/interfaces.js'; +import { Particle, serializeToString } from '../particle/Particle.js'; +import { IStartable } from '../util/commonTypes.js'; const log = logger('connection'); export const PROTOCOL_NAME = '/fluence/particle/2.0.0'; /** - * Options to configure fluence connection + * Options to configure fluence relay connection */ -export interface FluenceConnectionOptions { +export interface RelayConnectionConfig { /** * Peer id of the Fluence Peer */ @@ -50,32 +54,57 @@ export interface FluenceConnectionOptions { /** * Multiaddress of the relay to make connection to */ - relayAddress: MultiaddrInput; + relayAddress: Multiaddr; /** * The dialing timeout in milliseconds */ dialTimeoutMs?: number; + + /** + * The maximum number of inbound streams for the libp2p node. + * Default: 1024 + */ + maxInboundStreams: number; + + /** + * The maximum number of outbound streams for the libp2p node. + * Default: 1024 + */ + maxOutboundStreams: number; } /** * Implementation for JS peers which connects to Fluence through relay node */ -export class RelayConnection extends FluenceConnection { - constructor( - public peerId: PeerIdB58, - private _lib2p2Peer: Libp2p, - private _relayAddress: Multiaddr, - public readonly relayPeerId: PeerIdB58, - ) { - super(); +export class RelayConnection implements IStartable, IConnection { + private relayAddress: Multiaddr; + private lib2p2Peer: Libp2p | null = null; + + constructor(private config: RelayConnectionConfig) { + this.relayAddress = multiaddr(this.config.relayAddress); + throwIfHasNoPeerId(this.relayAddress); } - private _connection?: Connection; + getRelayPeerId(): string { + // since we check for peer id in constructor, we can safely use ! here + return this.relayAddress.getPeerId()!; + } + + supportsRelay(): boolean { + return true; + } + + particleSource = new Subject(); + + async start(): Promise { + // check if already started + if (this.lib2p2Peer !== null) { + return; + } - static async createConnection(options: FluenceConnectionOptions): Promise { const lib2p2Peer = await createLibp2p({ - peerId: options.peerId, + peerId: this.config.peerId, transports: [ webSockets({ filter: all, @@ -83,30 +112,32 @@ export class RelayConnection extends FluenceConnection { ], streamMuxers: [mplex()], connectionEncryption: [noise()], + connectionManager: { + dialTimeout: this.config.dialTimeoutMs, + }, }); - const relayMultiaddr = multiaddr(options.relayAddress); - const relayPeerId = relayMultiaddr.getPeerId(); - if (relayPeerId === null) { - throw new Error('Specified multiaddr is invalid or missing peer id: ' + options.relayAddress); + this.lib2p2Peer = lib2p2Peer; + this.lib2p2Peer.start(); + await this.connect(); + } + + async stop(): Promise { + // check if already stopped + if (this.lib2p2Peer === null) { + return; } - return new RelayConnection( - // force new line - options.peerId.toString(), - lib2p2Peer, - relayMultiaddr, - relayPeerId, - ); + await this.lib2p2Peer.unhandle(PROTOCOL_NAME); + await this.lib2p2Peer.stop(); } - async disconnect() { - await this._lib2p2Peer.unhandle(PROTOCOL_NAME); - await this._lib2p2Peer.stop(); - } + async sendParticle(nextPeerIds: PeerIdB58[], particle: IParticle): Promise { + if (this.lib2p2Peer === null) { + throw new Error('Relay connection is not started'); + } - async sendParticle(nextPeerIds: PeerIdB58[], particle: string): Promise { - if (nextPeerIds.length !== 1 && nextPeerIds[0] !== this.relayPeerId) { + if (nextPeerIds.length !== 1 && nextPeerIds[0] !== this.getRelayPeerId()) { throw new Error( `Relay connection only accepts peer id of the connected relay. Got: ${JSON.stringify( nextPeerIds, @@ -123,27 +154,23 @@ export class RelayConnection extends FluenceConnection { const sink = this._connection.streams[0].sink; */ - const stream = await this._lib2p2Peer.dialProtocol(this._relayAddress, PROTOCOL_NAME); + const stream = await this.lib2p2Peer.dialProtocol(this.relayAddress, PROTOCOL_NAME); const sink = stream.sink; pipe( - [fromString(particle)], + [fromString(serializeToString(particle))], // @ts-ignore encode(), sink, ); } - async connect(onIncomingParticle: ParticleHandler) { - await this._lib2p2Peer.start(); + private async connect() { + if (this.lib2p2Peer === null) { + throw new Error('Relay connection is not started'); + } - // TODO: make it configurable - const handleOptions = { - maxInboundStreams: 1024, - maxOutboundStreams: 1024, - }; - - this._lib2p2Peer.handle( + this.lib2p2Peer.handle( [PROTOCOL_NAME], async ({ connection, stream }) => { pipe( @@ -156,7 +183,8 @@ export class RelayConnection extends FluenceConnection { try { for await (const msg of source) { try { - onIncomingParticle(msg); + const particle = Particle.fromString(msg); + this.particleSource.next(particle); } catch (e) { log.error('error on handling a new incoming message: %j', e); } @@ -167,17 +195,20 @@ export class RelayConnection extends FluenceConnection { }, ); }, - handleOptions, + { + maxInboundStreams: this.config.maxInboundStreams, + maxOutboundStreams: this.config.maxOutboundStreams, + }, ); - log.debug("dialing to the node with client's address: %s", this._lib2p2Peer.peerId.toString()); + log.debug("dialing to the node with client's address: %s", this.lib2p2Peer.peerId.toString()); try { - this._connection = await this._lib2p2Peer.dial(this._relayAddress); + await this.lib2p2Peer.dial(this.relayAddress); } catch (e: any) { if (e.name === 'AggregateError' && e._errors?.length === 1) { const error = e._errors[0]; - throw new Error(`Error dialing node ${this._relayAddress}:\n${error.code}\n${error.message}`); + throw new Error(`Error dialing node ${this.relayAddress}:\n${error.code}\n${error.message}`); } else { throw e; } diff --git a/packages/core/js-peer/src/connection/interfaces.ts b/packages/core/js-peer/src/connection/interfaces.ts new file mode 100644 index 00000000..83282fe6 --- /dev/null +++ b/packages/core/js-peer/src/connection/interfaces.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { PeerIdB58 } from '@fluencelabs/interfaces'; +import type { Subscribable } from 'rxjs'; +import { IParticle } from '../particle/interfaces.js'; + +/** + * Interface for connection used in Fluence Peer. + */ +export interface IConnection { + /** + * Observable that emits particles received from the connection. + */ + particleSource: Subscribable; + + /** + * Send particle to the network using the connection. + * @param nextPeerIds - list of peer ids to send the particle to + * @param particle - particle to send + */ + sendParticle(nextPeerIds: PeerIdB58[], particle: IParticle): Promise; + + /** + * Get peer id of the relay peer. Throws an error if the connection doesn't support relay. + */ + getRelayPeerId(): PeerIdB58; + + /** + * Check if the connection supports relay. + */ + supportsRelay(): boolean; +} diff --git a/packages/core/js-peer/src/ephemeral/__test__/ephemeral.spec.ts b/packages/core/js-peer/src/ephemeral/__test__/ephemeral.spec.ts new file mode 100644 index 00000000..ee4d489b --- /dev/null +++ b/packages/core/js-peer/src/ephemeral/__test__/ephemeral.spec.ts @@ -0,0 +1,80 @@ +import { it, describe, expect, beforeEach, afterEach } from 'vitest'; +import { DEFAULT_CONFIG, FluencePeer } from '../../jsPeer/FluencePeer.js'; +import { CallServiceData, ResultCodes } from '../../jsServiceHost/interfaces.js'; +import { KeyPair } from '../../keypair/index.js'; +import { EphemeralNetworkClient } from '../client.js'; + +import { EphemeralNetwork, defaultConfig } from '../network.js'; + +let en: EphemeralNetwork; +let client: FluencePeer; +const relay = defaultConfig.peers[0].peerId; + +// TODO: race condition here. Needs to be fixed +describe.skip('Ephemeral networks tests', () => { + beforeEach(async () => { + en = new EphemeralNetwork(defaultConfig); + await en.up(); + + const kp = await KeyPair.randomEd25519(); + client = new EphemeralNetworkClient(DEFAULT_CONFIG, kp, en, relay); + await client.start(); + }); + + afterEach(async () => { + if (client) { + await client.stop(); + } + if (en) { + await en.down(); + } + }); + + it('smoke test', async function () { + // arrange + const peers = defaultConfig.peers.map((x) => x.peerId); + + const script = ` + (seq + (call "${relay}" ("op" "noop") []) + (seq + (call "${peers[1]}" ("op" "noop") []) + (seq + (call "${peers[2]}" ("op" "noop") []) + (seq + (call "${peers[3]}" ("op" "noop") []) + (seq + (call "${peers[4]}" ("op" "noop") []) + (seq + (call "${peers[5]}" ("op" "noop") []) + (seq + (call "${relay}" ("op" "noop") []) + (call %init_peer_id% ("test" "test") []) + ) + ) + ) + ) + ) + ) + ) + `; + + const particle = client.internals.createNewParticle(script); + + const promise = new Promise((resolve) => { + client.internals.regHandler.forParticle(particle.id, 'test', 'test', (req: CallServiceData) => { + resolve('success'); + return { + result: 'test', + retCode: ResultCodes.success, + }; + }); + }); + + // act + client.internals.initiateParticle(particle, () => {}); + + // assert + await expect(promise).resolves.toBe('success'); + }); +}); diff --git a/packages/core/js-peer/src/ephemeral/client.ts b/packages/core/js-peer/src/ephemeral/client.ts new file mode 100644 index 00000000..f30e12c5 --- /dev/null +++ b/packages/core/js-peer/src/ephemeral/client.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { PeerIdB58 } from '@fluencelabs/interfaces'; +import { MarineBasedAvmRunner } from '../jsPeer/avm.js'; +import { FluencePeer, PeerConfig } from '../jsPeer/FluencePeer.js'; +import { KeyPair } from '../keypair/index.js'; +import { WasmLoaderFromNpm } from '../marine/deps-loader/node.js'; +import { WorkerLoader } from '../marine/worker-script/workerLoader.js'; +import { MarineBackgroundRunner } from '../marine/worker/index.js'; +import { EphemeralNetwork } from './network.js'; +import { JsServiceHost } from '../jsServiceHost/JsServiceHost.js'; + +/** + * Ephemeral network client is a FluencePeer that connects to a relay peer in an ephemeral network. + */ +export class EphemeralNetworkClient extends FluencePeer { + constructor(config: PeerConfig, keyPair: KeyPair, network: EphemeralNetwork, relay: PeerIdB58) { + const workerLoader = new WorkerLoader(); + const controlModuleLoader = new WasmLoaderFromNpm('@fluencelabs/marine-js', 'marine-js.wasm'); + const avmModuleLoader = new WasmLoaderFromNpm('@fluencelabs/avm', 'avm.wasm'); + const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader); + const avm = new MarineBasedAvmRunner(marine, avmModuleLoader); + const conn = network.getRelayConnection(keyPair.getPeerId(), relay); + super(config, keyPair, marine, new JsServiceHost(), avm, conn); + } +} diff --git a/packages/core/js-peer/src/ephemeral/network.ts b/packages/core/js-peer/src/ephemeral/network.ts new file mode 100644 index 00000000..79c86604 --- /dev/null +++ b/packages/core/js-peer/src/ephemeral/network.ts @@ -0,0 +1,290 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { PeerIdB58 } from '@fluencelabs/interfaces'; +import { fromBase64Sk, KeyPair } from '../keypair/index.js'; +import { MarineBackgroundRunner } from '../marine/worker/index.js'; + +import { WorkerLoaderFromFs } from '../marine/deps-loader/node.js'; + +import { logger } from '../util/logger.js'; +import { Subject } from 'rxjs'; +import { Particle } from '../particle/Particle.js'; + +import { WasmLoaderFromNpm } from '../marine/deps-loader/node.js'; +import { MarineBasedAvmRunner } from '../jsPeer/avm.js'; +import { DEFAULT_CONFIG, FluencePeer } from '../jsPeer/FluencePeer.js'; +import { IConnection } from '../connection/interfaces.js'; +import { IAvmRunner, IMarineHost } from '../marine/interfaces.js'; +import { JsServiceHost } from '../jsServiceHost/JsServiceHost.js'; + +const log = logger('ephemeral'); + +interface EphemeralConfig { + peers: Array<{ + peerId: PeerIdB58; + sk: string; + }>; +} + +export const defaultConfig = { + peers: [ + { + peerId: '12D3KooWJankP2PcEDYCZDdJ26JsU8BMRfdGWyGqbtFiWyoKVtmx', + sk: 'dWNAHhDVuFj9bEieILMu6TcCFRxBJdOPIvAWmf4sZQI=', + }, + { + peerId: '12D3KooWSBTB5sYxdwayUyTnqopBwABsnGFY3p4dTx5hABYDtJjV', + sk: 'dOmaxAeu4Th+MJ22vRDLMFTNbiDgKNXar9fW9ofAMgQ=', + }, + { + peerId: '12D3KooWQjwf781DJ41moW5RrZXypLdnTbo6aMsoA8QLctGGX8RB', + sk: 'TgzaLlxXuOMDNuuuTKEHUKsW0jM4AmX0gahFvkB1KgE=', + }, + { + peerId: '12D3KooWCXWTLFyY1mqKnNAhLQTsjW1zqDzCMbUs8M4a8zdz28HK', + sk: 'hiO2Ta8g2ibMQ7iu5yj9CfN+qQCwE8oRShjr7ortKww=', + }, + { + peerId: '12D3KooWPmZpf4ng6GMS39HLagxsXbjiTPLH5CFJpFAHyN6amw6V', + sk: 'LzJtOHTqxfrlHDW40BKiLfjai8JU4yW6/s2zrXLCcQE=', + }, + { + peerId: '12D3KooWKrx8PZxM1R9A8tp2jmrFf6c6q1ZQiWfD4QkNgh7fWSoF', + sk: 'XMhlk/xr1FPcp7sKQhS18doXlq1x16EMhBC2NGW2LQ4=', + }, + { + peerId: '12D3KooWCbJHvnzSZEXjR1UJmtSUozuJK13iRiCYHLN1gjvm4TZZ', + sk: 'KXPAIqxrSHr7v0ngv3qagcqivFvnQ0xd3s1/rKmi8QU=', + }, + { + peerId: '12D3KooWEvKe7WQHp42W4xhHRgTAWQjtDWyH38uJbLHAsMuTtYvD', + sk: 'GCYMAshGnsrNtrHhuT7ayzh5uCzX99J03PmAXoOcCgw=', + }, + { + peerId: '12D3KooWSznSHN3BGrSykBXkLkFsqo9SYB73wVauVdqeuRt562cC', + sk: 'UP+SEuznS0h259VbFquzyOJAQ4W5iIwhP+hd1PmUQQ0=', + }, + { + peerId: '12D3KooWF57jwbShfnT3c4dNfRDdGjr6SQ3B71m87UVpEpSWHFwi', + sk: '8dl+Crm5RSh0eh+LqLKwX8/Eo4QLpvIjfD8L0wzX4A4=', + }, + { + peerId: '12D3KooWBWrzpSg9nwMLBCa2cJubUjTv63Mfy6PYg9rHGbetaV5C', + sk: 'qolc1FcpJ+vHDon0HeXdUYnstjV1wiVx2p0mjblrfAg=', + }, + { + peerId: '12D3KooWNkLVU6juM8oyN2SVq5nBd2kp7Rf4uzJH1hET6vj6G5j6', + sk: 'vN6QzWILTM7hSHp+iGkKxiXcqs8bzlnH3FPaRaDGSQY=', + }, + { + peerId: '12D3KooWKo1YwGL5vivPiKJMJS7wjtB6B2nJNdSXPkSABT4NKBUU', + sk: 'YbDQ++bsor2kei7rYAsu2SbyoiOYPRzFRZWnNRUpBgQ=', + }, + { + peerId: '12D3KooWLUyBKmmNCyxaPkXoWcUFPcy5qrZsUo2E1tyM6CJmGJvC', + sk: 'ptB9eSFMKudAtHaFgDrRK/1oIMrhBujxbMw2Pzwx/wA=', + }, + { + peerId: '12D3KooWAEZXME4KMu9FvLezsJWDbYFe2zyujyMnDT1AgcAxgcCk', + sk: 'xtwTOKgAbDIgkuPf7RKiR7gYyZ1HY4mOgFMv3sOUcAQ=', + }, + { + peerId: '12D3KooWEhXetsFVAD9h2dRz9XgFpfidho1TCZVhFrczX8h8qgzY', + sk: '1I2MGuiKG1F4FDMiRihVOcOP2mxzOLWJ99MeexK27A4=', + }, + { + peerId: '12D3KooWDBfVNdMyV3hPEF4WLBmx9DwD2t2SYuqZ2mztYmDzZWM1', + sk: 'eqJ4Bp7iN4aBXgPH0ezwSg+nVsatkYtfrXv9obI0YQ0=', + }, + { + peerId: '12D3KooWSyY7wiSiR4vbXa1WtZawi3ackMTqcQhEPrvqtagoWPny', + sk: 'UVM3SBJhPYIY/gafpnd9/q/Fn9V4BE9zkgrvF1T7Pgc=', + }, + { + peerId: '12D3KooWFZmBMGG9PxTs9s6ASzkLGKJWMyPheA5ruaYc2FDkDTmv', + sk: '8RbZfEVpQhPVuhv64uqxENDuSoyJrslQoSQJznxsTQ0=', + }, + { + peerId: '12D3KooWBbhUaqqur6KHPunnKxXjY1daCtqJdy4wRji89LmAkVB4', + sk: 'RbgKmG6soWW9uOi7yRedm+0Qck3f3rw6MSnDP7AcBQs=', + }, + ], +}; + +export interface IEphemeralConnection extends IConnection { + readonly selfPeerId: PeerIdB58; + readonly connections: Map; + receiveParticle(particle: Particle): void; +} + +export class EphemeralConnection implements IConnection, IEphemeralConnection { + readonly selfPeerId: PeerIdB58; + readonly connections: Map = new Map(); + + constructor(selfPeerId: PeerIdB58) { + this.selfPeerId = selfPeerId; + } + + connectToOther(other: IEphemeralConnection) { + if (other.selfPeerId === this.selfPeerId) { + return; + } + + this.connections.set(other.selfPeerId, other); + other.connections.set(this.selfPeerId, this); + } + + disconnectFromOther(other: IEphemeralConnection) { + this.connections.delete(other.selfPeerId); + other.connections.delete(this.selfPeerId); + } + + disconnectFromAll() { + for (let other of this.connections.values()) { + this.disconnectFromOther(other); + } + } + + particleSource = new Subject(); + + receiveParticle(particle: Particle): void { + this.particleSource.next(Particle.fromString(particle.toString())); + } + + async sendParticle(nextPeerIds: string[], particle: Particle): Promise { + const from = this.selfPeerId; + for (let to of nextPeerIds) { + const destConnection = this.connections.get(to); + if (destConnection === undefined) { + log.error('peer %s has no connection with %s', from, to); + continue; + } + + // log.trace(`Sending particle from %s, to %j, particleId %s`, from, to, particle.id); + destConnection.receiveParticle(particle); + } + } + + getRelayPeerId(): string { + if (this.connections.size === 1) { + return this.connections.keys().next().value; + } + + throw new Error('relay is not supported in this Ephemeral network peer'); + } + + supportsRelay(): boolean { + return this.connections.size === 1; + } +} + +class EphemeralPeer extends FluencePeer { + ephemeralConnection: EphemeralConnection; + + constructor(keyPair: KeyPair, marine: IMarineHost, avm: IAvmRunner) { + const conn = new EphemeralConnection(keyPair.getPeerId()); + super(DEFAULT_CONFIG, keyPair, marine, new JsServiceHost(), avm, conn); + + this.ephemeralConnection = conn; + } +} + +/** + * Ephemeral network implementation. + * Ephemeral network is a virtual network which runs locally and focuses on p2p interaction by removing connectivity layer out of the equation. + */ +export class EphemeralNetwork { + private peers: Map = new Map(); + + workerLoader: WorkerLoaderFromFs; + controlModuleLoader: WasmLoaderFromNpm; + avmModuleLoader: WasmLoaderFromNpm; + + constructor(public readonly config: EphemeralConfig) { + // shared worker for all the peers + this.workerLoader = new WorkerLoaderFromFs('../../marine/worker-script'); + this.controlModuleLoader = new WasmLoaderFromNpm('@fluencelabs/marine-js', 'marine-js.wasm'); + this.avmModuleLoader = new WasmLoaderFromNpm('@fluencelabs/avm', 'avm.wasm'); + } + + /** + * Starts the Ephemeral network up + */ + async up(): Promise { + log.trace('starting ephemeral network up...'); + + const promises = this.config.peers.map(async (x) => { + const kp = await fromBase64Sk(x.sk); + const marine = new MarineBackgroundRunner(this.workerLoader, this.controlModuleLoader); + const avm = new MarineBasedAvmRunner(marine, this.avmModuleLoader); + const peerId = kp.getPeerId(); + if (peerId !== x.peerId) { + throw new Error(`Invalid config: peer id ${x.peerId} does not match the secret key ${x.sk}`); + } + + return new EphemeralPeer(kp, marine, avm); + }); + + const peers = await Promise.all(promises); + + for (let i = 0; i < peers.length; i++) { + for (let j = 0; j < i; j++) { + if (i === j) { + continue; + } + + peers[i].ephemeralConnection.connectToOther(peers[j].ephemeralConnection); + } + } + + const startPromises = peers.map((x) => x.start()); + await Promise.all(startPromises); + + for (let p of peers) { + this.peers.set(p.keyPair.getPeerId(), p); + } + } + + /** + * Shuts the ephemeral network down. Will disconnect all connected peers. + */ + async down(): Promise { + log.trace('shutting down ephemeral network...'); + const peers = Array.from(this.peers.entries()); + const promises = peers.map(async ([k, p]) => { + await p.ephemeralConnection.disconnectFromAll(); + await p.stop(); + }); + + await Promise.all(promises); + this.peers.clear(); + log.trace('ephemeral network shut down'); + } + + /** + * Gets a relay connection to the specified peer. + */ + getRelayConnection(peerId: PeerIdB58, relayPeerId: PeerIdB58): IConnection { + const relay = this.peers.get(relayPeerId); + if (relay === undefined) { + throw new Error(`Peer ${relayPeerId} is not found`); + } + + const res = new EphemeralConnection(peerId); + res.connectToOther(relay.ephemeralConnection); + return res; + } +} diff --git a/packages/core/js-peer/src/js-peer/FluencePeer.ts b/packages/core/js-peer/src/js-peer/FluencePeer.ts deleted file mode 100644 index 4c5bb504..00000000 --- a/packages/core/js-peer/src/js-peer/FluencePeer.ts +++ /dev/null @@ -1,802 +0,0 @@ -/* - * Copyright 2021 Fluence Labs Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import 'buffer'; - -import { RelayConnection } from '../connection/index.js'; -import { FluenceConnection, IAvmRunner, IMarine } from '../interfaces/index.js'; -import { fromOpts, KeyPair } from '../keypair/index.js'; -import { - CallServiceData, - CallServiceResult, - GenericCallServiceHandler, - ResultCodes, -} from '../interfaces/commonTypes.js'; -import type { - PeerIdB58, - IFluenceClient, - KeyPairOptions, - RelayOptions, - ClientOptions, - ConnectionState, -} from '@fluencelabs/interfaces/dist/fluenceClient'; -import { Particle, ParticleExecutionStage, ParticleQueueItem } from './Particle.js'; -import { jsonify, isString, ServiceError } from './utils.js'; -import { concatMap, filter, pipe, Subject, tap } from 'rxjs'; -import { builtInServices } from './builtins/common.js'; -import { defaultSigGuard, Sig } from './builtins/Sig.js'; -import { registerSig } from './_aqua/services.js'; -import { registerSrv } from './_aqua/single-module-srv.js'; -import { Buffer } from 'buffer'; - -import { JSONValue } from '@fluencelabs/avm'; -import { NodeUtils, Srv } from './builtins/SingleModuleSrv.js'; -import { registerNodeUtils } from './_aqua/node-utils.js'; -import type { MultiaddrInput } from '@multiformats/multiaddr'; - -import { logger } from '../util/logger.js'; - -const log = logger('particle'); - -const DEFAULT_TTL = 7000; - -export type PeerConfig = ClientOptions & { relay?: RelayOptions }; - -type PeerStatus = - | { - isInitialized: false; - peerId: null; - isConnected: false; - relayPeerId: null; - } - | { - isInitialized: true; - peerId: PeerIdB58; - isConnected: false; - relayPeerId: null; - } - | { - isInitialized: true; - peerId: PeerIdB58; - isConnected: true; - relayPeerId: PeerIdB58; - } - | { - isInitialized: true; - peerId: PeerIdB58; - isConnected: true; - isDirect: true; - relayPeerId: null; - }; - -/** - * This class implements the Fluence protocol for javascript-based environments. - * It provides all the necessary features to communicate with Fluence network - */ -export class FluencePeer implements IFluenceClient { - connectionState: ConnectionState = 'disconnected'; - connectionStateChangeHandler: (state: ConnectionState) => void = () => {}; - - constructor(private marine: IMarine, private avmRunner: IAvmRunner) {} - - /** - * Internal contract to cast unknown objects to IFluenceClient. - * If an unknown object has this property then we assume it is in fact a Peer and it implements IFluenceClient - * Check against this variable MUST NOT be coupled with any `FluencePeer` because otherwise it might get bundled - * brining a lot of unnecessary stuff alongside with it - */ - __isFluenceAwesome = true; - - /** - * TODO: remove this from here. Switch to `ConnectionState` instead - * @deprecated - */ - getStatus(): PeerStatus { - if (this._keyPair === undefined) { - return { - isInitialized: false, - peerId: null, - isConnected: false, - relayPeerId: null, - }; - } - - if (this.connection === null) { - return { - isInitialized: true, - peerId: this._keyPair.getPeerId(), - isConnected: false, - relayPeerId: null, - }; - } - - if (this.connection.relayPeerId === null) { - return { - isInitialized: true, - peerId: this._keyPair.getPeerId(), - isConnected: true, - isDirect: true, - relayPeerId: null, - }; - } - - return { - isInitialized: true, - peerId: this._keyPair.getPeerId(), - isConnected: true, - relayPeerId: this.connection.relayPeerId, - }; - } - - getPeerId(): string { - return this.getStatus().peerId!; - } - - getRelayPeerId(): string { - return this.getStatus().relayPeerId!; - } - - getPeerSecretKey(): Uint8Array { - if (!this._keyPair) { - throw new Error("Can't get key pair: peer is not initialized"); - } - - return this._keyPair.toEd25519PrivateKey(); - } - - onConnectionStateChange(handler: (state: ConnectionState) => void): ConnectionState { - this.connectionStateChangeHandler = handler; - - return this.connectionState; - } - - /** - * Connect to the Fluence network - * @param relay - relay node to connect to - * @param options - client options - */ - async connect(relay: RelayOptions, options?: ClientOptions): Promise { - return this.start({ relay, ...options }); - } - - /** - * Disconnect from the Fluence network - */ - disconnect(): Promise { - return this.stop(); - } - - /** - * Initializes the peer: starts the Aqua VM, initializes the default call service handlers - * and (optionally) connect to the Fluence network - * @param config - object specifying peer configuration - */ - async start(config: PeerConfig = {}): Promise { - this.changeConnectionState('connecting'); - const keyPair = await makeKeyPair(config.keyPair); - await this.init(config, keyPair); - - const conn = await configToConnection(keyPair, config.relay, config.connectionOptions?.dialTimeoutMs); - - if (conn !== null) { - await this._connect(conn); - } - this.changeConnectionState('connected'); - } - - getServices() { - if (this._classServices === undefined) { - throw new Error(`Can't get services: peer is not initialized`); - } - return { - ...this._classServices, - }; - } - - /** - * Registers marine service within the Fluence peer from wasm file. - * Following helper functions can be used to load wasm files: - * * loadWasmFromFileSystem - * * loadWasmFromNpmPackage - * * loadWasmFromServer - * @param wasm - buffer with the wasm file for service - * @param serviceId - the service id by which the service can be accessed in aqua - */ - async registerMarineService(wasm: SharedArrayBuffer | Buffer, serviceId: string): Promise { - if (!this.marine) { - throw new Error("Can't register marine service: peer is not initialized"); - } - if (this._containsService(serviceId)) { - throw new Error(`Service with '${serviceId}' id already exists`); - } - - await this.marine.createService(wasm, serviceId); - this._marineServices.add(serviceId); - } - - /** - * Removes the specified marine service from the Fluence peer - * @param serviceId - the service id to remove - */ - removeMarineService(serviceId: string): void { - this._marineServices.delete(serviceId); - } - - /** - * Un-initializes the peer: stops all the underlying workflows, stops the Aqua VM - * and disconnects from the Fluence network - */ - async stop() { - this.changeConnectionState('disconnecting'); - this._keyPair = undefined; // This will set peer to non-initialized state and stop particle processing - this._stopParticleProcessing(); - await this._disconnect(); - await this.marine.stop(); - await this.avmRunner.stop(); - this._classServices = undefined; - - this._particleSpecificHandlers.clear(); - this._commonHandlers.clear(); - this._marineServices.clear(); - this.changeConnectionState('disconnected'); - } - - // internal api - - /** - * @private Is not intended to be used manually. Subject to change - */ - get internals() { - return { - getConnectionState: () => this.connectionState, - - getRelayPeerId: () => this.getStatus().relayPeerId, - - parseAst: async (air: string): Promise<{ success: boolean; data: any }> => { - const status = this.getStatus(); - - if (!status.isInitialized) { - new Error("Can't use avm: peer is not initialized"); - } - - const res = await this.marine.callService('avm', 'ast', [air], undefined); - if (!isString(res)) { - throw new Error(`Call to avm:ast expected to return string. Actual return: ${res}`); - } - - try { - if (res.startsWith('error')) { - return { - success: false, - data: res, - }; - } else { - return { - success: true, - data: JSON.parse(res), - }; - } - } catch (err) { - throw new Error('Failed to call avm. Result: ' + res + '. Error: ' + err); - } - }, - createNewParticle: (script: string, ttl: number = this._defaultTTL) => { - const status = this.getStatus(); - - if (!status.isInitialized) { - return new Error("Can't create new particle: peer is not initialized"); - } - - return Particle.createNew(script, ttl, status.peerId); - }, - /** - * Initiates a new particle execution starting from local peer - * @param particle - particle to start execution of - */ - initiateParticle: (particle: Particle, onStageChange: (stage: ParticleExecutionStage) => void): void => { - const status = this.getStatus(); - if (!status.isInitialized) { - throw new Error('Cannot initiate new particle: peer is not initialized'); - } - - if (this._printParticleId) { - console.log('Particle id: ', particle.id); - } - - if (particle.initPeerId === undefined) { - particle.initPeerId = status.peerId; - } - - if (particle.ttl === undefined) { - particle.ttl = this._defaultTTL; - } - - this._incomingParticles.next({ - particle: particle, - onStageChange: onStageChange, - }); - }, - - /** - * Register Call Service handler functions - */ - regHandler: { - /** - * Register handler for all particles - */ - common: ( - // force new line - serviceId: string, - fnName: string, - handler: GenericCallServiceHandler, - ) => { - this._commonHandlers.set(serviceFnKey(serviceId, fnName), handler); - }, - /** - * Register handler which will be called only for particle with the specific id - */ - forParticle: ( - particleId: string, - serviceId: string, - fnName: string, - handler: GenericCallServiceHandler, - ) => { - let psh = this._particleSpecificHandlers.get(particleId); - if (psh === undefined) { - psh = new Map(); - this._particleSpecificHandlers.set(particleId, psh); - } - - psh.set(serviceFnKey(serviceId, fnName), handler); - }, - }, - }; - } - - /** - * @private Subject to change. Do not use this method directly - */ - async init(config: Omit, keyPair: KeyPair) { - this._keyPair = keyPair; - - const peerId = this._keyPair.getPeerId(); - - if (config?.debug?.printParticleId) { - this._printParticleId = true; - } - - this._defaultTTL = config?.defaultTtlMs ?? DEFAULT_TTL; - - await this.marine.start(); - await this.avmRunner.start(); - - registerDefaultServices(this); - - this._classServices = { - sig: new Sig(this._keyPair), - srv: new Srv(this), - }; - this._classServices.sig.securityGuard = defaultSigGuard(peerId); - registerSig(this, 'sig', this._classServices.sig); - registerSig(this, peerId, this._classServices.sig); - - registerSrv(this, 'single_module_srv', this._classServices.srv); - registerNodeUtils(this, 'node_utils', new NodeUtils(this)); - - this._startParticleProcessing(); - } - - /** - * @private Subject to change. Do not use this method directly - */ - async _connect(connection: FluenceConnection): Promise { - if (this.connection) { - await this.connection.disconnect(); - } - - this.connection = connection; - await this.connection.connect(this._onIncomingParticle.bind(this)); - } - - /** - * @private Subject to change. Do not use this method directly - */ - async _disconnect(): Promise { - await this.connection?.disconnect(); - } - - // private - - private changeConnectionState(state: ConnectionState) { - this.connectionState = state; - this.connectionStateChangeHandler(state); - } - - // Queues for incoming and outgoing particles - - private _incomingParticles = new Subject(); - private _outgoingParticles = new Subject(); - - // Call service handler - - private _marineServices = new Set(); - private _particleSpecificHandlers = new Map>(); - private _commonHandlers = new Map(); - - private _classServices?: { - sig: Sig; - srv: Srv; - }; - - private _containsService(serviceId: string): boolean { - return this._marineServices.has(serviceId) || this._commonHandlers.has(serviceId); - } - - // Internal peer state - - private connection: FluenceConnection | null = null; - private _printParticleId = false; - private _defaultTTL: number = DEFAULT_TTL; - private _keyPair: KeyPair | undefined; - private _timeouts: Array = []; - private _particleQueues = new Map>(); - - private _onIncomingParticle(p: string) { - const particle = Particle.fromString(p); - this._incomingParticles.next({ particle, onStageChange: () => {} }); - } - - private _startParticleProcessing() { - this._incomingParticles - .pipe( - tap((x) => { - log.debug('id %s. received:', x.particle.id); - log.trace('id %s. data: %j', x.particle.id, { - initPeerId: x.particle.initPeerId, - timestamp: x.particle.timestamp, - tttl: x.particle.ttl, - signature: x.particle.signature, - }); - - log.trace('id %s. script: %s', x.particle.id, x.particle.script); - log.trace('id %s. call results: %j', x.particle.id, x.particle.callResults); - }), - filterExpiredParticles(this._expireParticle.bind(this)), - ) - .subscribe((item) => { - const p = item.particle; - let particlesQueue = this._particleQueues.get(p.id); - - if (!particlesQueue) { - particlesQueue = this._createParticlesProcessingQueue(); - this._particleQueues.set(p.id, particlesQueue); - - const timeout = setTimeout(() => { - this._expireParticle(item); - }, p.actualTtl()); - - this._timeouts.push(timeout); - } - - particlesQueue.next(item); - }); - - this._outgoingParticles.subscribe((item) => { - // Do not send particle after the peer has been stopped - if (!this.getStatus().isInitialized) { - return; - } - - if (!this.connection) { - log.error('id %s. cannot send, peer is not connected', item.particle.id); - item.onStageChange({ stage: 'sendingError' }); - return; - } - log.debug('id %s. sending particle into network', item.particle.id); - this.connection - ?.sendParticle(item.nextPeerIds, item.particle.toString()) - .then(() => { - item.onStageChange({ stage: 'sent' }); - }) - .catch((e: any) => { - log.error('id %s. send failed %j', item.particle.id, e); - }); - }); - } - - private _expireParticle(item: ParticleQueueItem) { - const particleId = item.particle.id; - log.debug( - 'id %s. particle has expired after %d. Deleting particle-related queues and handlers', - item.particle.id, - item.particle.ttl, - ); - - this._particleQueues.delete(particleId); - this._particleSpecificHandlers.delete(particleId); - - item.onStageChange({ stage: 'expired' }); - } - - private _createParticlesProcessingQueue() { - const particlesQueue = new Subject(); - let prevData: Uint8Array = Buffer.from([]); - - particlesQueue - .pipe( - filterExpiredParticles(this._expireParticle.bind(this)), - - concatMap(async (item) => { - const status = this.getStatus(); - if (!status.isInitialized || this.marine === undefined) { - // If `.stop()` was called return null to stop particle processing immediately - return null; - } - - // IMPORTANT! - // AVM runner execution and prevData <-> newData swapping - // MUST happen sequentially (in a critical section). - // Otherwise the race might occur corrupting the prevData - - log.debug('id %s. sending particle to interpreter', item.particle.id); - log.trace('id %s. prevData: %a', item.particle.id, prevData); - const avmCallResult = await this.avmRunner.run( - { - initPeerId: item.particle.initPeerId, - currentPeerId: status.peerId, - timestamp: item.particle.timestamp, - ttl: item.particle.ttl, - }, - item.particle.script, - prevData, - item.particle.data, - item.particle.callResults, - ); - - if (!(avmCallResult instanceof Error) && avmCallResult.retCode === 0) { - const newData = Buffer.from(avmCallResult.data); - prevData = newData; - } - - return { - ...item, - result: avmCallResult, - }; - }), - ) - .subscribe((item) => { - // If `.stop()` was called then item will be null and we need to stop particle processing immediately - if (item === null || !this.getStatus().isInitialized) { - return; - } - - // Do not proceed further if the particle is expired - if (item.particle.hasExpired()) { - return; - } - - // Do not continue if there was an error in particle interpretation - if (item.result instanceof Error) { - log.error('id %s. interpreter failed: %s', item.particle.id, item.result.message); - item.onStageChange({ stage: 'interpreterError', errorMessage: item.result.message }); - return; - } - - if (item.result.retCode !== 0) { - log.error( - 'id %s. interpreter failed: retCode: %d, message: %s', - item.particle.id, - item.result.retCode, - item.result.errorMessage, - ); - log.trace('id %s. avm data: %a', item.particle.id, item.result.data); - item.onStageChange({ stage: 'interpreterError', errorMessage: item.result.errorMessage }); - return; - } - - log.trace( - 'id %s. interpreter result: retCode: %d, avm data: %a', - item.particle.id, - item.result.retCode, - item.result.data, - ); - - setTimeout(() => { - item.onStageChange({ stage: 'interpreted' }); - }, 0); - - // send particle further if requested - if (item.result.nextPeerPks.length > 0) { - const newParticle = item.particle.clone(); - const newData = Buffer.from(item.result.data); - newParticle.data = newData; - this._outgoingParticles.next({ - ...item, - particle: newParticle, - nextPeerIds: item.result.nextPeerPks, - }); - } - - // execute call requests if needed - // and put particle with the results back to queue - if (item.result.callRequests.length > 0) { - for (const [key, cr] of item.result.callRequests) { - const req = { - fnName: cr.functionName, - args: cr.arguments, - serviceId: cr.serviceId, - tetraplets: cr.tetraplets, - particleContext: item.particle.getParticleContext(), - }; - - if (item.particle.hasExpired()) { - // just in case do not call any services if the particle is already expired - return; - } - this._execSingleCallRequest(req) - .catch((err): CallServiceResult => { - if (err instanceof ServiceError) { - return { - retCode: ResultCodes.error, - result: err.message, - }; - } - - return { - retCode: ResultCodes.error, - result: `Handler failed. fnName="${req.fnName}" serviceId="${ - req.serviceId - }" error: ${err.toString()}`, - }; - }) - .then((res) => { - const serviceResult = { - result: jsonify(res.result), - retCode: res.retCode, - }; - - const newParticle = item.particle.clone(); - newParticle.callResults = [[key, serviceResult]]; - newParticle.data = Buffer.from([]); - - particlesQueue.next({ ...item, particle: newParticle }); - }); - } - } else { - item.onStageChange({ stage: 'localWorkDone' }); - } - }); - - return particlesQueue; - } - - private async _execSingleCallRequest(req: CallServiceData): Promise { - const particleId = req.particleContext.particleId; - log.trace('id %s. executing call service handler %j', particleId, req); - - if (this.marine && this._marineServices.has(req.serviceId)) { - const result = await this.marine.callService(req.serviceId, req.fnName, req.args, undefined); - - return { - retCode: ResultCodes.success, - result: result as JSONValue, - }; - } - - const key = serviceFnKey(req.serviceId, req.fnName); - const psh = this._particleSpecificHandlers.get(particleId); - let handler: GenericCallServiceHandler | undefined; - - // we should prioritize handler for this particle if there is one - // if particle-specific handlers exist for this particle try getting handler there - if (psh !== undefined) { - handler = psh.get(key); - } - - // then try to find a common handler for all particles with this service-fn key - // if there is no particle-specific handler, get one from common map - if (handler === undefined) { - handler = this._commonHandlers.get(key); - } - - // if no handler is found return useful error message to AVM - if (handler === undefined) { - return { - retCode: ResultCodes.error, - result: `No handler has been registered for serviceId='${req.serviceId}' fnName='${ - req.fnName - }' args='${jsonify(req.args)}'`, - }; - } - - // if we found a handler, execute it - const res = await handler(req); - - if (res.result === undefined) { - res.result = null; - } - - log.trace('id %s. executed call service handler, req: %j, res: %j ', particleId, req, res); - return res; - } - - private _stopParticleProcessing() { - // do not hang if the peer has been stopped while some of the timeouts are still being executed - this._timeouts.forEach((timeout) => { - clearTimeout(timeout); - }); - this._particleQueues.clear(); - } -} - -async function configToConnection( - keyPair: KeyPair, - connection?: RelayOptions, - dialTimeoutMs?: number, -): Promise { - if (!connection) { - return null; - } - - if (connection instanceof FluenceConnection) { - return connection; - } - - let connectToMultiAddr: MultiaddrInput; - // figuring out what was specified as input - const tmp = connection as any; - if (tmp.multiaddr !== undefined) { - // specified as FluenceNode (object with multiaddr and peerId props) - connectToMultiAddr = tmp.multiaddr; - } else { - // specified as MultiaddrInput - connectToMultiAddr = tmp; - } - - const res = await RelayConnection.createConnection({ - peerId: keyPair.getLibp2pPeerId(), - relayAddress: connectToMultiAddr, - dialTimeoutMs: dialTimeoutMs, - }); - return res; -} - -function serviceFnKey(serviceId: string, fnName: string) { - return `${serviceId}/${fnName}`; -} - -function registerDefaultServices(peer: FluencePeer) { - Object.entries(builtInServices).forEach(([serviceId, service]) => { - Object.entries(service).forEach(([fnName, fn]) => { - peer.internals.regHandler.common(serviceId, fnName, fn); - }); - }); -} - -function filterExpiredParticles(onParticleExpiration: (item: ParticleQueueItem) => void) { - return pipe( - tap((item: ParticleQueueItem) => { - if (item.particle.hasExpired()) { - onParticleExpiration(item); - } - }), - filter((x: ParticleQueueItem) => !x.particle.hasExpired()), - ); -} - -async function makeKeyPair(opts?: KeyPairOptions) { - opts = opts || { type: 'Ed25519', source: 'random' }; - return fromOpts(opts); -} diff --git a/packages/core/js-peer/src/js-peer/Particle.ts b/packages/core/js-peer/src/js-peer/Particle.ts deleted file mode 100644 index b5ed8846..00000000 --- a/packages/core/js-peer/src/js-peer/Particle.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2020 Fluence Labs Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { fromUint8Array, toUint8Array } from 'js-base64'; -import { CallResultsArray, LogLevel } from '@fluencelabs/avm'; -import { v4 as uuidv4 } from 'uuid'; -import { ParticleContext } from '../interfaces/commonTypes.js'; -import { Buffer } from 'buffer'; - -export class Particle { - // TODO: make it not optional (should be added to the constructor) - signature?: string; - callResults: CallResultsArray = []; - - constructor( - public id: string, - public timestamp: number, - public script: string, - public data: Uint8Array, - public ttl: number, - public initPeerId: string, - ) {} - - static createNew(script: string, ttl: number, initPeerId: string): Particle { - return new Particle(genUUID(), Date.now(), script, Buffer.from([]), ttl, initPeerId); - } - - static fromString(str: string): Particle { - const json = JSON.parse(str); - const res = new Particle( - json.id, - json.timestamp, - json.script, - toUint8Array(json.data), - json.ttl, - json.init_peer_id, - ); - - res.signature = json.signature; - - return res; - } - - getParticleContext(): ParticleContext { - return { - particleId: this.id, - initPeerId: this.initPeerId, - timestamp: this.timestamp, - ttl: this.ttl, - signature: this.signature, - }; - } - - actualTtl(): number { - return this.timestamp + this.ttl - Date.now(); - } - - hasExpired(): boolean { - return this.actualTtl() <= 0; - } - - clone(): Particle { - const res = new Particle(this.id, this.timestamp, this.script, this.data, this.ttl, this.initPeerId); - - res.signature = this.signature; - res.callResults = this.callResults; - return res; - } - - toString(): string { - return JSON.stringify({ - action: 'Particle', - id: this.id, - init_peer_id: this.initPeerId, - timestamp: this.timestamp, - ttl: this.ttl, - script: this.script, - // TODO: copy signature from a particle after signatures will be implemented on nodes - signature: [], - data: this.data && fromUint8Array(this.data), - }); - } -} - -export type ParticleExecutionStage = - | { stage: 'received' } - | { stage: 'interpreted' } - | { stage: 'interpreterError'; errorMessage: string } - | { stage: 'localWorkDone' } - | { stage: 'sent' } - | { stage: 'sendingError' } - | { stage: 'expired' }; - -export interface ParticleQueueItem { - particle: Particle; - onStageChange: (state: ParticleExecutionStage) => void; -} - -function genUUID() { - return uuidv4(); -} diff --git a/packages/core/js-peer/src/js-peer/__test__/integration/peer.spec.ts b/packages/core/js-peer/src/js-peer/__test__/integration/peer.spec.ts deleted file mode 100644 index 197fade2..00000000 --- a/packages/core/js-peer/src/js-peer/__test__/integration/peer.spec.ts +++ /dev/null @@ -1,417 +0,0 @@ -import { it, describe, expect } from 'vitest'; - -import { nodes } from '../connection.js'; -import { checkConnection, doNothing, handleTimeout } from '../../utils.js'; -import { registerHandlersHelper, mkTestPeer, withPeer, withConnectedPeer } from '../util.js'; -import { FluencePeer } from '../../FluencePeer.js'; -import { isFluencePeer } from '@fluencelabs/interfaces'; - -describe('Typescript usage suite', () => { - it('should perform test for FluencePeer class correctly', () => { - // arrange - const peer = mkTestPeer(); - const number = 1; - const object = { str: 'Hello!' }; - const undefinedVal = undefined; - - // act - const isPeerPeer = isFluencePeer(peer); - const isNumberPeer = isFluencePeer(number); - const isObjectPeer = isFluencePeer(object); - const isUndefinedPeer = isFluencePeer(undefinedVal); - - // act - expect(isPeerPeer).toBe(true); - expect(isNumberPeer).toBe(false); - expect(isObjectPeer).toBe(false); - expect(isUndefinedPeer).toBe(false); - }); - - describe('Should expose correct peer status', () => { - it('Should expose correct status for uninitialized peer', () => { - const peer = mkTestPeer(); - const status = peer.getStatus(); - - expect(status.isConnected).toBe(false); - expect(status.isInitialized).toBe(false); - expect(status.peerId).toBe(null); - expect(status.relayPeerId).toBe(null); - }); - - it('Should expose correct status for initialized but not connected peer', async () => { - await withPeer(async (peer) => { - // arrange - - // act - const status = peer.getStatus(); - - // assert - expect(status.isConnected).toBe(false); - expect(status.isInitialized).toBe(true); - expect(status.peerId).not.toBe(null); - expect(status.relayPeerId).toBe(null); - }); - }); - - it('Should expose correct status for connected peer', async () => { - await withConnectedPeer(async (peer) => { - // arrange - - // act - const status = peer.getStatus(); - - // assert - expect(status.isConnected).toBe(true); - expect(status.isInitialized).toBe(true); - expect(status.peerId).not.toBe(null); - expect(status.relayPeerId).not.toBe(null); - }); - }); - }); - - it('should make a call through network', async () => { - await withConnectedPeer(async (peer) => { - // arrange - - const result = await new Promise((resolve, reject) => { - const script = ` - (xor - (seq - (call %init_peer_id% ("load" "relay") [] init_relay) - (seq - (call init_relay ("op" "identity") ["hello world!"] result) - (call %init_peer_id% ("callback" "callback") [result]) - ) - ) - (seq - (call init_relay ("op" "identity") []) - (call %init_peer_id% ("callback" "error") [%last_error%]) - ) - )`; - const particle = peer.internals.createNewParticle(script); - - if (particle instanceof Error) { - return reject(particle.message); - } - - registerHandlersHelper(peer, particle, { - load: { - relay: () => { - return peer.getStatus().relayPeerId; - }, - }, - callback: { - callback: (args: any) => { - const [val] = args; - resolve(val); - }, - error: (args: any) => { - const [error] = args; - reject(error); - }, - }, - }); - - peer.internals.initiateParticle(particle, handleTimeout(reject)); - }); - - expect(result).toBe('hello world!'); - }); - }); - - it('check connection should work', async function () { - await withConnectedPeer(async (peer) => { - const isConnected = await checkConnection(peer); - - expect(isConnected).toEqual(true); - }); - }); - - it('check connection should work with ttl', async function () { - await withConnectedPeer(async (peer) => { - const isConnected = await checkConnection(peer, 10000); - - expect(isConnected).toEqual(true); - }); - }); - - it('two clients should work inside the same time browser', async () => { - await withConnectedPeer(async (peer1) => { - await withConnectedPeer(async (peer2) => { - const res = new Promise((resolve) => { - peer2.internals.regHandler.common('test', 'test', (req) => { - resolve(req.args[0]); - return { - result: {}, - retCode: 0, - }; - }); - }); - - const script = ` - (seq - (call "${peer1.getStatus().relayPeerId}" ("op" "identity") []) - (call "${peer2.getStatus().peerId}" ("test" "test") ["test"]) - ) - `; - const particle = peer1.internals.createNewParticle(script); - - if (particle instanceof Error) { - throw particle; - } - - peer1.internals.initiateParticle(particle, doNothing); - - expect(await res).toEqual('test'); - }); - }); - }); - - describe('should make connection to network', () => { - it('address as string', async () => { - await withConnectedPeer(async (peer) => { - const isConnected = await checkConnection(peer); - - expect(isConnected).toBeTruthy(); - }); - }); - - it('address as multiaddr', async () => { - await withConnectedPeer(async (peer) => { - const isConnected = await checkConnection(peer); - - expect(isConnected).toBeTruthy(); - }); - }); - - it('address as node', async () => { - await withConnectedPeer(async (peer) => { - const isConnected = await checkConnection(peer); - - expect(isConnected).toBeTruthy(); - }); - }); - - it('With connection options: dialTimeout', async () => { - await withPeer( - async (peer) => { - const isConnected = await checkConnection(peer); - - expect(isConnected).toBeTruthy(); - }, - { relay: nodes[0], connectionOptions: { dialTimeoutMs: 100000 } }, - ); - }); - - it('With connection options: skipCheckConnection', async () => { - await withPeer( - async (peer) => { - const isConnected = await checkConnection(peer); - - expect(isConnected).toBeTruthy(); - }, - { relay: nodes[0], connectionOptions: { skipCheckConnection: true } }, - ); - }); - - it('With connection options: defaultTTL', async () => { - await withPeer( - async (peer) => { - const isConnected = await checkConnection(peer); - - expect(isConnected).toBeFalsy(); - }, - { relay: nodes[0], defaultTtlMs: 1 }, - ); - }); - }); - - it('Should successfully call identity on local peer', async function () { - await withPeer(async (peer) => { - const res = await new Promise((resolve, reject) => { - const script = ` - (seq - (call %init_peer_id% ("op" "identity") ["test"] res) - (call %init_peer_id% ("callback" "callback") [res]) - ) - `; - const particle = peer.internals.createNewParticle(script); - - if (particle instanceof Error) { - return reject(particle.message); - } - - registerHandlersHelper(peer, particle, { - callback: { - callback: async (args: any) => { - const [res] = args; - resolve(res); - }, - }, - }); - - peer.internals.initiateParticle(particle, handleTimeout(reject)); - }); - - expect(res).toBe('test'); - }); - }); - - it('Should throw correct message when calling non existing local service', async function () { - await withConnectedPeer(async (peer) => { - const res = callIncorrectService(peer); - - await expect(res).rejects.toMatchObject({ - message: expect.stringContaining( - `No handler has been registered for serviceId='incorrect' fnName='incorrect' args='[]'\"'`, - ), - // instruction: 'call %init_peer_id% ("incorrect" "incorrect") [] res', - }); - }); - }); - - it('Should not crash if undefined is passed as a variable', async () => { - await withPeer(async (peer) => { - const res = await new Promise((resolve, reject) => { - const script = ` - (seq - (call %init_peer_id% ("load" "arg") [] arg) - (seq - (call %init_peer_id% ("op" "identity") [arg] res) - (call %init_peer_id% ("callback" "callback") [res]) - ) - )`; - const particle = peer.internals.createNewParticle(script); - - if (particle instanceof Error) { - return reject(particle.message); - } - - registerHandlersHelper(peer, particle, { - load: { - arg: () => undefined, - }, - callback: { - callback: (args: any) => { - const [val] = args; - resolve(val); - }, - error: (args: any) => { - const [error] = args; - reject(error); - }, - }, - }); - - peer.internals.initiateParticle(particle, handleTimeout(reject)); - }); - - expect(res).toBe(null); - }); - }); - - it('Should not crash if an error ocurred in user-defined handler', async () => { - await withPeer(async (peer) => { - const promise = new Promise((_resolve, reject) => { - const script = ` - (xor - (call %init_peer_id% ("load" "arg") [] arg) - (call %init_peer_id% ("callback" "error") [%last_error%]) - )`; - const particle = peer.internals.createNewParticle(script); - - if (particle instanceof Error) { - return reject(particle.message); - } - - registerHandlersHelper(peer, particle, { - load: { - arg: () => { - throw new Error('my super custom error message'); - }, - }, - callback: { - error: (args: any) => { - const [error] = args; - reject(error); - }, - }, - }); - - peer.internals.initiateParticle(particle, handleTimeout(reject)); - }); - - await expect(promise).rejects.toMatchObject({ - message: expect.stringContaining('my super custom error message'), - }); - }); - }); - - it('Should return error if particle is created on a stopped peer', async () => { - const peer = mkTestPeer(); - const particle = peer.internals.createNewParticle(`(null)`); - - expect(particle instanceof Error).toBe(true); - }); - - it.skip('Should throw correct error when the client tries to send a particle not to the relay', async () => { - await withConnectedPeer(async (peer) => { - const promise = new Promise((resolve, reject) => { - const script = ` - (xor - (call "incorrect_peer_id" ("any" "service") []) - (call %init_peer_id% ("callback" "error") [%last_error%]) - )`; - const particle = peer.internals.createNewParticle(script); - - if (particle instanceof Error) { - return reject(particle.message); - } - - registerHandlersHelper(peer, particle, { - callback: { - error: (args: any) => { - const [error] = args; - reject(error); - }, - }, - }); - - peer.internals.initiateParticle(particle, doNothing); - }); - - await expect(promise).rejects.toMatch( - 'Particle is expected to be sent to only the single peer (relay which client is connected to)', - ); - }); - }); -}); - -async function callIncorrectService(peer: FluencePeer): Promise { - return new Promise((resolve, reject) => { - const script = ` - (xor - (call %init_peer_id% ("incorrect" "incorrect") [] res) - (call %init_peer_id% ("callback" "error") [%last_error%]) - )`; - const particle = peer.internals.createNewParticle(script); - - if (particle instanceof Error) { - return reject(particle.message); - } - - registerHandlersHelper(peer, particle, { - callback: { - callback: (args: any) => { - resolve(args); - }, - error: (args: any) => { - const [error] = args; - reject(error); - }, - }, - }); - - peer.internals.initiateParticle(particle, handleTimeout(reject)); - }); -} diff --git a/packages/core/js-peer/src/js-peer/__test__/integration/smokeTest.spec.ts b/packages/core/js-peer/src/js-peer/__test__/integration/smokeTest.spec.ts deleted file mode 100644 index 7f93340c..00000000 --- a/packages/core/js-peer/src/js-peer/__test__/integration/smokeTest.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { it, describe, expect } from 'vitest'; - -import { handleTimeout } from '../../utils.js'; -import { nodes } from '../connection.js'; -import { mkTestPeer, registerHandlersHelper } from '../util.js'; - -describe('Smoke test', () => { - it('Simple call', async () => { - // arrange - const peer = mkTestPeer(); - await peer.start({ - relay: nodes[0], - }); - - const result = await new Promise((resolve, reject) => { - const script = ` - (xor - (seq - (call %init_peer_id% ("load" "relay") [] init_relay) - (seq - (call init_relay ("op" "identity") ["hello world!"] result) - (call %init_peer_id% ("callback" "callback") [result]) - ) - ) - (seq - (call init_relay ("op" "identity") []) - (call %init_peer_id% ("callback" "error") [%last_error%]) - ) - )`; - const particle = peer.internals.createNewParticle(script); - - if (particle instanceof Error) { - return reject(particle.message); - } - - registerHandlersHelper(peer, particle, { - load: { - relay: () => { - return peer.getStatus().relayPeerId; - }, - }, - callback: { - callback: (args: any) => { - const [val] = args; - resolve(val); - }, - error: (args: any) => { - const [error] = args; - reject(error); - }, - }, - }); - - peer.internals.initiateParticle(particle, handleTimeout(reject)); - }); - - await peer.stop(); - - expect(result).toBe('hello world!'); - }); -}); diff --git a/packages/core/js-peer/src/js-peer/__test__/unit/ast.spec.ts b/packages/core/js-peer/src/js-peer/__test__/unit/ast.spec.ts deleted file mode 100644 index a8798d79..00000000 --- a/packages/core/js-peer/src/js-peer/__test__/unit/ast.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { it, describe, expect, beforeAll, afterAll } from 'vitest'; - -import { mkTestPeer } from '../util.js'; - -const peer = mkTestPeer(); - -describe('Parse ast tests', () => { - beforeAll(async () => { - await peer.start(); - }); - - afterAll(async () => { - await peer.stop(); - }); - - it('Correct ast should be parsed correctly', async function () { - const air = `(null)`; - const res = await peer.internals.parseAst(air); - - expect(res).toStrictEqual({ - success: true, - data: { Null: null }, - }); - }); - - it('Incorrect ast should result in corresponding error', async function () { - const air = `(null`; - const res = await peer.internals.parseAst(air); - - expect(res).toStrictEqual({ - success: false, - data: expect.stringContaining('error'), - }); - }); -}); diff --git a/packages/core/js-peer/src/js-peer/__test__/unit/ephemeral.spec.ts.skip b/packages/core/js-peer/src/js-peer/__test__/unit/ephemeral.spec.ts.skip deleted file mode 100644 index 9005b6aa..00000000 --- a/packages/core/js-peer/src/js-peer/__test__/unit/ephemeral.spec.ts.skip +++ /dev/null @@ -1,84 +0,0 @@ -import { KeyPair } from '@fluencelabs/keypair'; -import { EphemeralNetwork, defaultConfig } from '../../ephemeral'; -import { ResultCodes } from '../../commonTypes'; -import { FluencePeer } from '../../FluencePeer'; -import { mkTestPeer } from '../util'; - -let en: EphemeralNetwork; -let peer: FluencePeer; - -// TODO: jest tests hang when running this test. Fix it (DXJ-219) -describe.skip('Ephemeral networks tests', () => { - beforeEach(async () => { - en = new EphemeralNetwork(defaultConfig); - await en.up(); - const relay = defaultConfig.peers[0].peerId; - - peer = mkTestPeer(); - await peer.init({ - KeyPair: await KeyPair.randomEd25519(), - }); - - const conn = en.getRelayConnection(relay, peer); - await peer.connect(conn); - }); - - afterEach(async () => { - if (peer) { - await peer.stop(); - } - if (en) { - await en.down(); - } - }); - - it('smoke test', async function () { - const relay = peer.getStatus().relayPeerId!; - - const peers = defaultConfig.peers.map((x) => x.peerId); - - const script = ` - (seq - (call "${relay}" ("op" "noop") []) - (seq - (call "${peers[0]}" ("op" "noop") []) - (seq - (call "${peers[1]}" ("op" "noop") []) - (seq - (call "${peers[2]}" ("op" "noop") []) - (seq - (call "${peers[3]}" ("op" "noop") []) - (seq - (call "${peers[4]}" ("op" "noop") []) - (seq - (call "${relay}" ("op" "noop") []) - (call %init_peer_id% ("test" "test") []) - ) - ) - ) - ) - ) - ) - ) - `; - - const particle = peer.internals.createNewParticle(script); - if (particle instanceof Error) { - throw particle; - } - - const promise = new Promise((resolve) => { - peer.internals.regHandler.forParticle(particle.id, 'test', 'test', (req) => { - resolve('success'); - return { - result: 'test', - retCode: ResultCodes.success, - }; - }); - }); - - peer.internals.initiateParticle(particle, () => {}); - - await expect(promise).resolves.toBe('success'); - }); -}); diff --git a/packages/core/js-peer/src/js-peer/__test__/util.ts b/packages/core/js-peer/src/js-peer/__test__/util.ts deleted file mode 100644 index 7054f551..00000000 --- a/packages/core/js-peer/src/js-peer/__test__/util.ts +++ /dev/null @@ -1,83 +0,0 @@ -import * as api from '@fluencelabs/aqua-api/aqua-api.js'; - -import { promises as fs } from 'fs'; -import { FluencePeer, PeerConfig } from '../FluencePeer.js'; -import { Particle } from '../Particle.js'; -import { MakeServiceCall } from '../utils.js'; -import { avmModuleLoader, controlModuleLoader } from '../utilsForNode.js'; -import { ServiceDef } from '@fluencelabs/interfaces'; -import { callAquaFunction } from '../../compilerSupport/callFunction.js'; - -import { MarineBackgroundRunner } from '../../marine/worker/index.js'; -import { MarineBasedAvmRunner } from '../avm.js'; -import { nodes } from './connection.js'; -import { WorkerLoaderFromFs } from '../../marine/deps-loader/node.js'; - -export const registerHandlersHelper = ( - peer: FluencePeer, - particle: Particle, - handlers: Record>, -) => { - Object.entries(handlers).forEach(([serviceId, service]) => { - Object.entries(service).forEach(([fnName, fn]) => { - peer.internals.regHandler.forParticle(particle.id, serviceId, fnName, MakeServiceCall(fn)); - }); - }); -}; - -export type CompiledFnCall = (peer: FluencePeer, args: { [key: string]: any }) => Promise; -export type CompiledFile = { - functions: { [key: string]: CompiledFnCall }; - services: { [key: string]: ServiceDef }; -}; - -export const compileAqua = async (aquaFile: string): Promise => { - await fs.access(aquaFile); - - const compilationResult = await api.Aqua.compile(new api.Path(aquaFile), [], undefined); - - if (compilationResult.errors.length > 0) { - throw new Error('Aqua compilation failed. Error: ' + compilationResult.errors.join('/n')); - } - - const functions = Object.entries(compilationResult.functions) - .map(([name, fnInfo]) => { - const callFn = (peer: FluencePeer, args: { [key: string]: any }) => { - return callAquaFunction({ - def: fnInfo.funcDef, - script: fnInfo.script, - config: {}, - peer: peer, - args, - }); - }; - return { [name]: callFn }; - }) - .reduce((agg, obj) => { - return { ...agg, ...obj }; - }, {}); - - return { functions, services: compilationResult.services }; -}; - -export const mkTestPeer = () => { - const workerLoader = new WorkerLoaderFromFs('../../marine/worker-script'); - - const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader); - const avm = new MarineBasedAvmRunner(marine, avmModuleLoader); - return new FluencePeer(marine, avm); -}; - -export const withPeer = async (action: (p: FluencePeer) => Promise, config?: PeerConfig) => { - const p = mkTestPeer(); - try { - await p.start(config); - await action(p); - } finally { - await p!.stop(); - } -}; - -export const withConnectedPeer = async (action: (p: FluencePeer) => Promise, config?: PeerConfig) => { - return withPeer(action, { relay: nodes[0] }); -}; diff --git a/packages/core/js-peer/src/js-peer/_aqua/util.ts b/packages/core/js-peer/src/js-peer/_aqua/util.ts deleted file mode 100644 index 49288273..00000000 --- a/packages/core/js-peer/src/js-peer/_aqua/util.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IFluenceClient, ServiceDef } from '@fluencelabs/interfaces'; -import { registerService } from '../../compilerSupport/registerService.js'; - -export const registerServiceImpl = ( - peer: IFluenceClient, - def: ServiceDef, - serviceId: string | undefined, - service: any, -) => registerService({ peer, def, service, serviceId }); diff --git a/packages/core/js-peer/src/js-peer/ephemeral.ts b/packages/core/js-peer/src/js-peer/ephemeral.ts deleted file mode 100644 index 3718ce55..00000000 --- a/packages/core/js-peer/src/js-peer/ephemeral.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { PeerIdB58 } from '@fluencelabs/interfaces'; -import { FluenceConnection, ParticleHandler } from '../interfaces/index.js'; -import { fromBase64Sk } from '../keypair/index.js'; -import { FluencePeer } from './FluencePeer.js'; -import { MarineBackgroundRunner } from '../marine/worker/index.js'; -import { avmModuleLoader, controlModuleLoader } from './utilsForNode.js'; -import { MarineBasedAvmRunner } from './avm.js'; - -import { WorkerLoaderFromFs } from '../marine/deps-loader/node.js'; - -import { logger } from '../util/logger.js'; - -interface EphemeralConfig { - peers: Array<{ - peerId: PeerIdB58; - sk: string; - }>; -} - -interface PeerAdapter { - isEphemeral: boolean; - peer: FluencePeer; - peerId: PeerIdB58; - onIncoming: ParticleHandler; - connections: Set; -} - -const log = logger('ephemeral'); - -export const defaultConfig = { - peers: [ - { - peerId: '12D3KooWJankP2PcEDYCZDdJ26JsU8BMRfdGWyGqbtFiWyoKVtmx', - sk: 'dWNAHhDVuFj9bEieILMu6TcCFRxBJdOPIvAWmf4sZQI=', - }, - { - peerId: '12D3KooWSBTB5sYxdwayUyTnqopBwABsnGFY3p4dTx5hABYDtJjV', - sk: 'dOmaxAeu4Th+MJ22vRDLMFTNbiDgKNXar9fW9ofAMgQ=', - }, - { - peerId: '12D3KooWQjwf781DJ41moW5RrZXypLdnTbo6aMsoA8QLctGGX8RB', - sk: 'TgzaLlxXuOMDNuuuTKEHUKsW0jM4AmX0gahFvkB1KgE=', - }, - { - peerId: '12D3KooWCXWTLFyY1mqKnNAhLQTsjW1zqDzCMbUs8M4a8zdz28HK', - sk: 'hiO2Ta8g2ibMQ7iu5yj9CfN+qQCwE8oRShjr7ortKww=', - }, - { - peerId: '12D3KooWPmZpf4ng6GMS39HLagxsXbjiTPLH5CFJpFAHyN6amw6V', - sk: 'LzJtOHTqxfrlHDW40BKiLfjai8JU4yW6/s2zrXLCcQE=', - }, - { - peerId: '12D3KooWKrx8PZxM1R9A8tp2jmrFf6c6q1ZQiWfD4QkNgh7fWSoF', - sk: 'XMhlk/xr1FPcp7sKQhS18doXlq1x16EMhBC2NGW2LQ4=', - }, - { - peerId: '12D3KooWCbJHvnzSZEXjR1UJmtSUozuJK13iRiCYHLN1gjvm4TZZ', - sk: 'KXPAIqxrSHr7v0ngv3qagcqivFvnQ0xd3s1/rKmi8QU=', - }, - { - peerId: '12D3KooWEvKe7WQHp42W4xhHRgTAWQjtDWyH38uJbLHAsMuTtYvD', - sk: 'GCYMAshGnsrNtrHhuT7ayzh5uCzX99J03PmAXoOcCgw=', - }, - { - peerId: '12D3KooWSznSHN3BGrSykBXkLkFsqo9SYB73wVauVdqeuRt562cC', - sk: 'UP+SEuznS0h259VbFquzyOJAQ4W5iIwhP+hd1PmUQQ0=', - }, - { - peerId: '12D3KooWF57jwbShfnT3c4dNfRDdGjr6SQ3B71m87UVpEpSWHFwi', - sk: '8dl+Crm5RSh0eh+LqLKwX8/Eo4QLpvIjfD8L0wzX4A4=', - }, - { - peerId: '12D3KooWBWrzpSg9nwMLBCa2cJubUjTv63Mfy6PYg9rHGbetaV5C', - sk: 'qolc1FcpJ+vHDon0HeXdUYnstjV1wiVx2p0mjblrfAg=', - }, - { - peerId: '12D3KooWNkLVU6juM8oyN2SVq5nBd2kp7Rf4uzJH1hET6vj6G5j6', - sk: 'vN6QzWILTM7hSHp+iGkKxiXcqs8bzlnH3FPaRaDGSQY=', - }, - { - peerId: '12D3KooWKo1YwGL5vivPiKJMJS7wjtB6B2nJNdSXPkSABT4NKBUU', - sk: 'YbDQ++bsor2kei7rYAsu2SbyoiOYPRzFRZWnNRUpBgQ=', - }, - { - peerId: '12D3KooWLUyBKmmNCyxaPkXoWcUFPcy5qrZsUo2E1tyM6CJmGJvC', - sk: 'ptB9eSFMKudAtHaFgDrRK/1oIMrhBujxbMw2Pzwx/wA=', - }, - { - peerId: '12D3KooWAEZXME4KMu9FvLezsJWDbYFe2zyujyMnDT1AgcAxgcCk', - sk: 'xtwTOKgAbDIgkuPf7RKiR7gYyZ1HY4mOgFMv3sOUcAQ=', - }, - { - peerId: '12D3KooWEhXetsFVAD9h2dRz9XgFpfidho1TCZVhFrczX8h8qgzY', - sk: '1I2MGuiKG1F4FDMiRihVOcOP2mxzOLWJ99MeexK27A4=', - }, - { - peerId: '12D3KooWDBfVNdMyV3hPEF4WLBmx9DwD2t2SYuqZ2mztYmDzZWM1', - sk: 'eqJ4Bp7iN4aBXgPH0ezwSg+nVsatkYtfrXv9obI0YQ0=', - }, - { - peerId: '12D3KooWSyY7wiSiR4vbXa1WtZawi3ackMTqcQhEPrvqtagoWPny', - sk: 'UVM3SBJhPYIY/gafpnd9/q/Fn9V4BE9zkgrvF1T7Pgc=', - }, - { - peerId: '12D3KooWFZmBMGG9PxTs9s6ASzkLGKJWMyPheA5ruaYc2FDkDTmv', - sk: '8RbZfEVpQhPVuhv64uqxENDuSoyJrslQoSQJznxsTQ0=', - }, - { - peerId: '12D3KooWBbhUaqqur6KHPunnKxXjY1daCtqJdy4wRji89LmAkVB4', - sk: 'RbgKmG6soWW9uOi7yRedm+0Qck3f3rw6MSnDP7AcBQs=', - }, - ], -}; - -/** - * Ephemeral network implementation. - * Ephemeral network is a virtual network which runs locally and focuses on p2p interaction by removing connectivity layer out of the equation. - */ -export class EphemeralNetwork { - private _peers: Map = new Map(); - - constructor(public readonly config: EphemeralConfig) {} - - /** - * Starts the Ephemeral network up - */ - async up(): Promise { - log.trace('starting ephemeral network up...'); - const allPeerIds = this.config.peers.map((x) => x.peerId); - // shared worker for all the peers - const workerLoader = new WorkerLoaderFromFs('../../marine/worker-script'); - - const promises = this.config.peers.map(async (x) => { - const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader); - const avm = new MarineBasedAvmRunner(marine, avmModuleLoader); - const peer = new FluencePeer(marine, avm); - const sendParticle = async (nextPeerIds: string[], particle: string): Promise => { - this._send(peer.getStatus().peerId!, nextPeerIds, particle); - }; - const kp = await fromBase64Sk(x.sk); - if (kp.getPeerId() !== x.peerId) { - throw new Error(`Invalid config: peer id ${x.peerId} does not match the secret key ${x.sk}`); - } - await peer.init({}, kp); - - let handler: ParticleHandler | null = null; - const connectionCtor = class extends FluenceConnection { - relayPeerId = null; - - async connect(onIncomingParticle: ParticleHandler): Promise { - handler = onIncomingParticle; - } - - async disconnect(): Promise { - handler = null; - } - - sendParticle = sendParticle; - }; - - await peer._connect(new connectionCtor()); - - const peerId = peer.getStatus().peerId!; - const ephPeer: PeerAdapter = { - isEphemeral: true, - connections: new Set(allPeerIds.filter((x) => x !== peerId)), - peer: peer, - peerId: peerId, - onIncoming: handler!, - }; - return [peerId, ephPeer] as const; - }); - const values = await Promise.all(promises); - this._peers = new Map(values); - log.trace('ephemeral network started...'); - } - - /** - * Shuts the ephemeral network down. Will disconnect all connected peers. - */ - async down(): Promise { - log.trace('shutting down ephemeral network...'); - const peers = Array.from(this._peers.entries()); - const promises = peers.map(([k, p]) => { - return p.isEphemeral ? p.peer.stop() : p.peer._disconnect(); - }); - await Promise.all(promises); - this._peers.clear(); - log.trace('ephemeral network shut down'); - } - - /** - * Gets the FluenceConnection which can be used to connect to the ephemeral networks via the specified relay peer. - */ - getRelayConnection(relay: PeerIdB58, peer: FluencePeer): FluenceConnection { - const me = this; - const relayPeer = this._peers.get(relay); - if (relayPeer === undefined) { - throw new Error(`Relay with peer Id: ${relay} has not been found in ephemeral network`); - } - const connectionCtor = class extends FluenceConnection { - relayPeerId = relay; - - async connect(onIncomingParticle: ParticleHandler): Promise { - const peerId = peer.getStatus().peerId!; - me._peers.set(peerId, { - isEphemeral: false, - peer: peer, - onIncoming: onIncomingParticle, - peerId: peerId, - connections: new Set([relay]), - }); - relayPeer.connections.add(peerId); - } - - async disconnect(): Promise { - const peerId = peer.getStatus().peerId!; - relayPeer.connections.delete(peerId); - me._peers.delete(peerId); - } - async sendParticle(nextPeerIds: string[], particle: string): Promise { - const peerId = peer.getStatus().peerId!; - me._send(peerId, nextPeerIds, particle); - } - }; - return new connectionCtor(); - } - - private async _send(from: PeerIdB58, to: PeerIdB58[], particle: string) { - log.trace(`Sending particle from %s, to %j`, from, to); - const peer = this._peers.get(from); - if (peer === undefined) { - log.error(`Peer ${from} cannot be found in ephemeral network`); - return; - } - - for (let dest of to) { - if (!peer.connections.has(dest)) { - log.error(`Peer ${from} has no connection with ${dest}`); - continue; - } - - const destPeer = this._peers.get(dest); - if (destPeer === undefined) { - log.error(`peer ${destPeer} cannot be found in ephemeral network`); - continue; - } - - destPeer.onIncoming(particle); - } - } -} diff --git a/packages/core/js-peer/src/js-peer/utilsForNode.ts b/packages/core/js-peer/src/js-peer/utilsForNode.ts deleted file mode 100644 index 22b3018f..00000000 --- a/packages/core/js-peer/src/js-peer/utilsForNode.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { WorkerLoaderFromFs, WasmLoaderFromFs, WasmLoaderFromNpm } from '../marine/deps-loader/node.js'; - -// TODO!: after moving to ESM loaders stopped working. Should be fixed in scope of DXJ-194 -export const controlModuleLoader = new WasmLoaderFromNpm('@fluencelabs/marine-js', 'marine-js.wasm'); -export const avmModuleLoader = new WasmLoaderFromNpm('@fluencelabs/avm', 'avm.wasm'); diff --git a/packages/core/js-peer/src/jsPeer/FluencePeer.ts b/packages/core/js-peer/src/jsPeer/FluencePeer.ts new file mode 100644 index 00000000..758d6612 --- /dev/null +++ b/packages/core/js-peer/src/jsPeer/FluencePeer.ts @@ -0,0 +1,557 @@ +/* + * Copyright 2021 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { KeyPair } from '../keypair/index.js'; + +import type { PeerIdB58 } from '@fluencelabs/interfaces'; +import { + cloneWithNewData, + getActualTTL, + hasExpired, + Particle, + ParticleExecutionStage, + ParticleQueueItem, +} from '../particle/Particle.js'; +import { jsonify, isString } from '../util/utils.js'; +import { concatMap, filter, pipe, Subject, tap, Unsubscribable } from 'rxjs'; +import { defaultSigGuard, Sig } from '../services/Sig.js'; +import { registerSig } from '../services/_aqua/services.js'; +import { registerSrv } from '../services/_aqua/single-module-srv.js'; +import { Buffer } from 'buffer'; + +import { Srv } from '../services/SingleModuleSrv.js'; + +import { logger } from '../util/logger.js'; +import { getParticleContext, registerDefaultServices, ServiceError } from '../jsServiceHost/serviceUtils.js'; +import { IParticle } from '../particle/interfaces.js'; +import { IConnection } from '../connection/interfaces.js'; +import { IAvmRunner, IMarineHost } from '../marine/interfaces.js'; +import { + CallServiceData, + CallServiceResult, + GenericCallServiceHandler, + IJsServiceHost, + ResultCodes, +} from '../jsServiceHost/interfaces.js'; +import { JSONValue } from '../util/commonTypes.js'; + +const log_particle = logger('particle'); +const log_peer = logger('peer'); + +export type PeerConfig = { + /** + * Sets the default TTL for all particles originating from the peer with no TTL specified. + * If the originating particle's TTL is defined then that value will be used + * If the option is not set default TTL will be 7000 + */ + defaultTtlMs: number; + + /** + * Enables\disabled various debugging features + */ + debug: { + /** + * If set to true, newly initiated particle ids will be printed to console. + * Useful to see what particle id is responsible for aqua function + */ + printParticleId: boolean; + }; +}; + +export const DEFAULT_CONFIG: PeerConfig = { + debug: { + printParticleId: false, + }, + defaultTtlMs: 7000, +}; + +/** + * This class implements the Fluence protocol for javascript-based environments. + * It provides all the necessary features to communicate with Fluence network + */ +export abstract class FluencePeer { + constructor( + protected readonly config: PeerConfig, + public readonly keyPair: KeyPair, + protected readonly marineHost: IMarineHost, + protected readonly jsServiceHost: IJsServiceHost, + protected readonly avmRunner: IAvmRunner, + protected readonly connection: IConnection, + ) { + this._initServices(); + } + + /** + * Internal contract to cast unknown objects to IFluenceClient. + * If an unknown object has this property then we assume it is in fact a Peer and it implements IFluenceClient + * Check against this variable MUST NOT be coupled with any `FluencePeer` because otherwise it might get bundled + * brining a lot of unnecessary stuff alongside with it + */ + __isFluenceAwesome = true; + + async start(): Promise { + log_peer.trace('starting Fluence peer'); + if (this.config?.debug?.printParticleId) { + this.printParticleId = true; + } + + await this.marineHost.start(); + await this.avmRunner.start(); + + this._startParticleProcessing(); + this.isInitialized = true; + log_peer.trace('started Fluence peer'); + } + + /** + * Un-initializes the peer: stops all the underlying workflows, stops the Aqua VM + * and disconnects from the Fluence network + */ + async stop() { + log_peer.trace('stopping Fluence peer'); + this._particleSourceSubscription?.unsubscribe(); + this._stopParticleProcessing(); + await this.marineHost.stop(); + await this.avmRunner.stop(); + + this.isInitialized = false; + log_peer.trace('stopped Fluence peer'); + } + + /** + * Registers marine service within the Fluence peer from wasm file. + * Following helper functions can be used to load wasm files: + * * loadWasmFromFileSystem + * * loadWasmFromNpmPackage + * * loadWasmFromServer + * @param wasm - buffer with the wasm file for service + * @param serviceId - the service id by which the service can be accessed in aqua + */ + async registerMarineService(wasm: SharedArrayBuffer | Buffer, serviceId: string): Promise { + if (!this.marineHost) { + throw new Error("Can't register marine service: peer is not initialized"); + } + + if (this.jsServiceHost.hasService(serviceId)) { + throw new Error(`Service with '${serviceId}' id already exists`); + } + + await this.marineHost.createService(wasm, serviceId); + } + + /** + * Removes the specified marine service from the Fluence peer + * @param serviceId - the service id to remove + */ + removeMarineService(serviceId: string): void { + this.marineHost.removeService(serviceId); + } + + // internal api + + /** + * @private Is not intended to be used manually. Subject to change + */ + get internals() { + return { + getServices: () => this._classServices, + + getRelayPeerId: () => { + if (this.connection.supportsRelay()) { + return this.connection.getRelayPeerId(); + } + + throw new Error('Relay is not supported by the current connection'); + }, + + parseAst: async (air: string): Promise<{ success: boolean; data: any }> => { + if (!this.isInitialized) { + new Error("Can't use avm: peer is not initialized"); + } + + const res = await this.marineHost.callService('avm', 'ast', [air], undefined); + if (!isString(res)) { + throw new Error(`Call to avm:ast expected to return string. Actual return: ${res}`); + } + + try { + if (res.startsWith('error')) { + return { + success: false, + data: res, + }; + } else { + return { + success: true, + data: JSON.parse(res), + }; + } + } catch (err) { + throw new Error('Failed to call avm. Result: ' + res + '. Error: ' + err); + } + }, + + createNewParticle: (script: string, ttl: number = this.config.defaultTtlMs): IParticle => { + return Particle.createNew(script, this.keyPair.getPeerId(), ttl); + }, + + /** + * Initiates a new particle execution starting from local peer + * @param particle - particle to start execution of + */ + initiateParticle: (particle: IParticle, onStageChange: (stage: ParticleExecutionStage) => void): void => { + if (!this.isInitialized) { + throw new Error('Cannot initiate new particle: peer is not initialized'); + } + + if (this.printParticleId) { + console.log('Particle id: ', particle.id); + } + + this._incomingParticles.next({ + particle: particle, + callResults: [], + onStageChange: onStageChange, + }); + }, + + /** + * Register Call Service handler functions + */ + regHandler: { + /** + * Register handler for all particles + */ + common: this.jsServiceHost.registerGlobalHandler.bind(this.jsServiceHost), + /** + * Register handler which will be called only for particle with the specific id + */ + forParticle: this.jsServiceHost.registerParticleScopeHandler.bind(this.jsServiceHost), + }, + }; + } + + // Queues for incoming and outgoing particles + + private _incomingParticles = new Subject(); + private _outgoingParticles = new Subject(); + private _timeouts: Array = []; + private _particleSourceSubscription?: Unsubscribable; + private _particleQueues = new Map>(); + + // Internal peer state + + // @ts-expect-error - initialized in constructor through `_initServices` call + private _classServices: { + sig: Sig; + srv: Srv; + }; + + private isInitialized = false; + private printParticleId = false; + + private _initServices() { + this._classServices = { + sig: new Sig(this.keyPair), + srv: new Srv(this), + }; + + const peerId = this.keyPair.getPeerId(); + + registerDefaultServices(this); + + this._classServices.sig.securityGuard = defaultSigGuard(peerId); + registerSig(this, 'sig', this._classServices.sig); + registerSig(this, peerId, this._classServices.sig); + + registerSrv(this, 'single_module_srv', this._classServices.srv); + } + + private _startParticleProcessing() { + this._particleSourceSubscription = this.connection.particleSource.subscribe({ + next: (p) => { + this._incomingParticles.next({ particle: p, callResults: [], onStageChange: () => {} }); + }, + }); + + this._incomingParticles + .pipe( + tap((item) => { + log_particle.debug('id %s. received:', item.particle.id); + log_particle.trace('id %s. data: %j', item.particle.id, { + initPeerId: item.particle.initPeerId, + timestamp: item.particle.timestamp, + tttl: item.particle.ttl, + signature: item.particle.signature, + }); + + log_particle.trace('id %s. script: %s', item.particle.id, item.particle.script); + log_particle.trace('id %s. call results: %j', item.particle.id, item.callResults); + }), + filterExpiredParticles(this._expireParticle.bind(this)), + ) + .subscribe((item) => { + const p = item.particle; + let particlesQueue = this._particleQueues.get(p.id); + + if (!particlesQueue) { + particlesQueue = this._createParticlesProcessingQueue(); + this._particleQueues.set(p.id, particlesQueue); + + const timeout = setTimeout(() => { + this._expireParticle(item); + }, getActualTTL(p)); + + this._timeouts.push(timeout); + } + + particlesQueue.next(item); + }); + + this._outgoingParticles.subscribe((item) => { + // Do not send particle after the peer has been stopped + if (!this.isInitialized) { + return; + } + + log_particle.debug( + 'id %s. sending particle into network. Next peer ids: %s', + item.particle.id, + item.nextPeerIds.toString(), + ); + + this.connection + ?.sendParticle(item.nextPeerIds, item.particle) + .then(() => { + item.onStageChange({ stage: 'sent' }); + }) + .catch((e: any) => { + log_particle.error('id %s. send failed %j', item.particle.id, e); + item.onStageChange({ stage: 'sendingError', errorMessage: e.toString() }); + }); + }); + } + + private _expireParticle(item: ParticleQueueItem) { + const particleId = item.particle.id; + log_particle.debug( + 'id %s. particle has expired after %d. Deleting particle-related queues and handlers', + item.particle.id, + item.particle.ttl, + ); + + this._particleQueues.delete(particleId); + this.jsServiceHost.removeParticleScopeHandlers(particleId); + + item.onStageChange({ stage: 'expired' }); + } + + private _createParticlesProcessingQueue() { + const particlesQueue = new Subject(); + let prevData: Uint8Array = Buffer.from([]); + + particlesQueue + .pipe( + filterExpiredParticles(this._expireParticle.bind(this)), + + concatMap(async (item) => { + if (!this.isInitialized || this.marineHost === undefined) { + // If `.stop()` was called return null to stop particle processing immediately + return null; + } + + // IMPORTANT! + // AVM runner execution and prevData <-> newData swapping + // MUST happen sequentially (in a critical section). + // Otherwise the race might occur corrupting the prevData + + log_particle.debug('id %s. sending particle to interpreter', item.particle.id); + log_particle.trace('id %s. prevData: %a', item.particle.id, prevData); + const avmCallResult = await this.avmRunner.run( + { + initPeerId: item.particle.initPeerId, + currentPeerId: this.keyPair.getPeerId(), + timestamp: item.particle.timestamp, + ttl: item.particle.ttl, + }, + item.particle.script, + prevData, + item.particle.data, + item.callResults, + ); + + if (!(avmCallResult instanceof Error) && avmCallResult.retCode === 0) { + const newData = Buffer.from(avmCallResult.data); + prevData = newData; + } + + return { + ...item, + result: avmCallResult, + }; + }), + ) + .subscribe((item) => { + // If peer was stopped, do not proceed further + if (item === null || !this.isInitialized) { + return; + } + + // Do not proceed further if the particle is expired + if (hasExpired(item.particle)) { + return; + } + + // Do not continue if there was an error in particle interpretation + if (item.result instanceof Error) { + log_particle.error('id %s. interpreter failed: %s', item.particle.id, item.result.message); + item.onStageChange({ stage: 'interpreterError', errorMessage: item.result.message }); + return; + } + + if (item.result.retCode !== 0) { + log_particle.error( + 'id %s. interpreter failed: retCode: %d, message: %s', + item.particle.id, + item.result.retCode, + item.result.errorMessage, + ); + log_particle.trace('id %s. avm data: %a', item.particle.id, item.result.data); + item.onStageChange({ stage: 'interpreterError', errorMessage: item.result.errorMessage }); + return; + } + + log_particle.trace( + 'id %s. interpreter result: retCode: %d, avm data: %a', + item.particle.id, + item.result.retCode, + item.result.data, + ); + + setTimeout(() => { + item.onStageChange({ stage: 'interpreted' }); + }, 0); + + // send particle further if requested + if (item.result.nextPeerPks.length > 0) { + const newParticle = cloneWithNewData(item.particle, Buffer.from(item.result.data)); + this._outgoingParticles.next({ + ...item, + particle: newParticle, + nextPeerIds: item.result.nextPeerPks, + }); + } + + // execute call requests if needed + // and put particle with the results back to queue + if (item.result.callRequests.length > 0) { + for (const [key, cr] of item.result.callRequests) { + const req = { + fnName: cr.functionName, + args: cr.arguments, + serviceId: cr.serviceId, + tetraplets: cr.tetraplets, + particleContext: getParticleContext(item.particle), + }; + + if (hasExpired(item.particle)) { + // just in case do not call any services if the particle is already expired + return; + } + this._execSingleCallRequest(req) + .catch((err): CallServiceResult => { + if (err instanceof ServiceError) { + return { + retCode: ResultCodes.error, + result: err.message, + }; + } + + return { + retCode: ResultCodes.error, + result: `Service call failed. fnName="${req.fnName}" serviceId="${ + req.serviceId + }" error: ${err.toString()}`, + }; + }) + .then((res) => { + const serviceResult = { + result: jsonify(res.result), + retCode: res.retCode, + }; + + const newParticle = cloneWithNewData(item.particle, Buffer.from([])); + particlesQueue.next({ + ...item, + particle: newParticle, + callResults: [[key, serviceResult]], + }); + }); + } + } else { + item.onStageChange({ stage: 'localWorkDone' }); + } + }); + + return particlesQueue; + } + + private async _execSingleCallRequest(req: CallServiceData): Promise { + const particleId = req.particleContext.particleId; + log_particle.trace('id %s. executing call service handler %j', particleId, req); + + if (this.marineHost && this.marineHost.hasService(req.serviceId)) { + const result = await this.marineHost.callService(req.serviceId, req.fnName, req.args, undefined); + + return { + retCode: ResultCodes.success, + result: result as JSONValue, + }; + } + + let res = await this.jsServiceHost.callService(req); + + if (res === null) { + res = { + retCode: ResultCodes.error, + result: `No service found for service call: serviceId='${req.serviceId}', fnName='${ + req.fnName + }' args='${jsonify(req.args)}'`, + }; + } + + log_particle.trace('id %s. executed call service handler, req: %j, res: %j ', particleId, req, res); + return res; + } + + private _stopParticleProcessing() { + // do not hang if the peer has been stopped while some of the timeouts are still being executed + this._timeouts.forEach((timeout) => { + clearTimeout(timeout); + }); + this._particleQueues.clear(); + } +} + +function filterExpiredParticles(onParticleExpiration: (item: ParticleQueueItem) => void) { + return pipe( + tap((item: ParticleQueueItem) => { + if (hasExpired(item.particle)) { + onParticleExpiration(item); + } + }), + filter((x: ParticleQueueItem) => !hasExpired(x.particle)), + ); +} diff --git a/packages/core/js-peer/src/js-peer/__test__/integration/avm.spec.ts b/packages/core/js-peer/src/jsPeer/__test__/avm.spec.ts similarity index 96% rename from packages/core/js-peer/src/js-peer/__test__/integration/avm.spec.ts rename to packages/core/js-peer/src/jsPeer/__test__/avm.spec.ts index 27846db5..543c3a30 100644 --- a/packages/core/js-peer/src/js-peer/__test__/integration/avm.spec.ts +++ b/packages/core/js-peer/src/jsPeer/__test__/avm.spec.ts @@ -1,9 +1,8 @@ import { it, describe, expect } from 'vitest'; +import { registerHandlersHelper, withPeer } from '../../util/testUtils.js'; +import { handleTimeout } from '../../particle/Particle.js'; -import { handleTimeout } from '../../utils.js'; -import { registerHandlersHelper, withPeer } from '../util.js'; - -describe('Avm spec', () => { +describe('Basic AVM functionality in Fluence Peer tests', () => { it('Simple call', async () => { await withPeer(async (peer) => { const res = await new Promise((resolve, reject) => { diff --git a/packages/core/js-peer/src/jsPeer/__test__/parseAst.spec.ts b/packages/core/js-peer/src/jsPeer/__test__/parseAst.spec.ts new file mode 100644 index 00000000..7c66043e --- /dev/null +++ b/packages/core/js-peer/src/jsPeer/__test__/parseAst.spec.ts @@ -0,0 +1,29 @@ +import { it, describe, expect } from 'vitest'; + +import { withPeer } from '../../util/testUtils.js'; + +describe('Parse ast tests', () => { + it('Correct ast should be parsed correctly', async () => { + withPeer(async (peer) => { + const air = `(null)`; + const res = await peer.internals.parseAst(air); + + expect(res).toStrictEqual({ + success: true, + data: { Null: null }, + }); + }); + }); + + it('Incorrect ast should result in corresponding error', async () => { + withPeer(async (peer) => { + const air = `(null`; + const res = await peer.internals.parseAst(air); + + expect(res).toStrictEqual({ + success: false, + data: expect.stringContaining('error'), + }); + }); + }); +}); diff --git a/packages/core/js-peer/src/jsPeer/__test__/peer.spec.ts b/packages/core/js-peer/src/jsPeer/__test__/peer.spec.ts new file mode 100644 index 00000000..8045af40 --- /dev/null +++ b/packages/core/js-peer/src/jsPeer/__test__/peer.spec.ts @@ -0,0 +1,178 @@ +import { it, describe, expect } from 'vitest'; + +import { isFluencePeer } from '@fluencelabs/interfaces'; +import { mkTestPeer, registerHandlersHelper, withPeer } from '../../util/testUtils.js'; +import { handleTimeout } from '../../particle/Particle.js'; +import { FluencePeer } from '../FluencePeer.js'; + +describe('FluencePeer usage test suite', () => { + it('should perform test for FluencePeer class correctly', async () => { + // arrange + const peer = await mkTestPeer(); + const number = 1; + const object = { str: 'Hello!' }; + const undefinedVal = undefined; + + // act + const isPeerPeer = isFluencePeer(peer); + const isNumberPeer = isFluencePeer(number); + const isObjectPeer = isFluencePeer(object); + const isUndefinedPeer = isFluencePeer(undefinedVal); + + // act + expect(isPeerPeer).toBe(true); + expect(isNumberPeer).toBe(false); + expect(isObjectPeer).toBe(false); + expect(isUndefinedPeer).toBe(false); + }); + + it('Should successfully call identity on local peer', async function () { + await withPeer(async (peer) => { + const res = await new Promise((resolve, reject) => { + const script = ` + (seq + (call %init_peer_id% ("op" "identity") ["test"] res) + (call %init_peer_id% ("callback" "callback") [res]) + ) + `; + const particle = peer.internals.createNewParticle(script); + + if (particle instanceof Error) { + return reject(particle.message); + } + + registerHandlersHelper(peer, particle, { + callback: { + callback: async (args: any) => { + const [res] = args; + resolve(res); + }, + }, + }); + + peer.internals.initiateParticle(particle, handleTimeout(reject)); + }); + + expect(res).toBe('test'); + }); + }); + + it('Should throw correct message when calling non existing local service', async function () { + await withPeer(async (peer) => { + const res = callIncorrectService(peer); + + await expect(res).rejects.toMatchObject({ + message: expect.stringContaining( + `"No service found for service call: serviceId='incorrect', fnName='incorrect' args='[]'"`, + ), + instruction: 'call %init_peer_id% ("incorrect" "incorrect") [] res', + }); + }); + }); + + it('Should not crash if undefined is passed as a variable', async () => { + await withPeer(async (peer) => { + const res = await new Promise((resolve, reject) => { + const script = ` + (seq + (call %init_peer_id% ("load" "arg") [] arg) + (seq + (call %init_peer_id% ("op" "identity") [arg] res) + (call %init_peer_id% ("callback" "callback") [res]) + ) + )`; + const particle = peer.internals.createNewParticle(script); + + if (particle instanceof Error) { + return reject(particle.message); + } + + registerHandlersHelper(peer, particle, { + load: { + arg: () => undefined, + }, + callback: { + callback: (args: any) => { + const [val] = args; + resolve(val); + }, + error: (args: any) => { + const [error] = args; + reject(error); + }, + }, + }); + + peer.internals.initiateParticle(particle, handleTimeout(reject)); + }); + + expect(res).toBe(null); + }); + }); + + it('Should not crash if an error ocurred in user-defined handler', async () => { + await withPeer(async (peer) => { + const promise = new Promise((_resolve, reject) => { + const script = ` + (xor + (call %init_peer_id% ("load" "arg") [] arg) + (call %init_peer_id% ("callback" "error") [%last_error%]) + )`; + const particle = peer.internals.createNewParticle(script); + + if (particle instanceof Error) { + return reject(particle.message); + } + + registerHandlersHelper(peer, particle, { + load: { + arg: () => { + throw new Error('my super custom error message'); + }, + }, + callback: { + error: (args: any) => { + const [error] = args; + reject(error); + }, + }, + }); + + peer.internals.initiateParticle(particle, handleTimeout(reject)); + }); + + await expect(promise).rejects.toMatchObject({ + message: expect.stringContaining('my super custom error message'), + }); + }); + }); +}); + +async function callIncorrectService(peer: FluencePeer): Promise { + return new Promise((resolve, reject) => { + const script = ` + (xor + (call %init_peer_id% ("incorrect" "incorrect") [] res) + (call %init_peer_id% ("callback" "error") [%last_error%]) + )`; + const particle = peer.internals.createNewParticle(script); + + if (particle instanceof Error) { + return reject(particle.message); + } + + registerHandlersHelper(peer, particle, { + callback: { + callback: (args: any) => { + resolve(args); + }, + error: (args: any) => { + const [error] = args; + reject(error); + }, + }, + }); + + peer.internals.initiateParticle(particle, handleTimeout(reject)); + }); +} diff --git a/packages/core/js-peer/src/js-peer/avm.ts b/packages/core/js-peer/src/jsPeer/avm.ts similarity index 59% rename from packages/core/js-peer/src/js-peer/avm.ts rename to packages/core/js-peer/src/jsPeer/avm.ts index b154130b..d9b4c578 100644 --- a/packages/core/js-peer/src/js-peer/avm.ts +++ b/packages/core/js-peer/src/jsPeer/avm.ts @@ -1,9 +1,24 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import type { CallResultsArray, InterpreterResult, RunParameters } from '@fluencelabs/avm'; import { deserializeAvmResult, serializeAvmArgs } from '@fluencelabs/avm'; -import type { IMarine, IAvmRunner, IWasmLoader } from '../interfaces/index.js'; +import { IAvmRunner, IMarineHost, IWasmLoader } from '../marine/interfaces.js'; export class MarineBasedAvmRunner implements IAvmRunner { - constructor(private marine: IMarine, private avmWasmLoader: IWasmLoader) {} + constructor(private marine: IMarineHost, private avmWasmLoader: IWasmLoader) {} async run( runParams: RunParameters, diff --git a/packages/core/js-peer/src/jsServiceHost/JsServiceHost.ts b/packages/core/js-peer/src/jsServiceHost/JsServiceHost.ts new file mode 100644 index 00000000..fd41de18 --- /dev/null +++ b/packages/core/js-peer/src/jsServiceHost/JsServiceHost.ts @@ -0,0 +1,111 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { CallServiceData, CallServiceResult, GenericCallServiceHandler, IJsServiceHost } from './interfaces.js'; + +export class JsServiceHost implements IJsServiceHost { + private particleScopeHandlers = new Map>(); + private commonHandlers = new Map(); + + /** + * Returns true if any handler for the specified serviceId is registered + */ + hasService(serviceId: string): boolean { + return this.commonHandlers.has(serviceId) || this.particleScopeHandlers.has(serviceId); + } + + /** + * Removes all handlers associated with the specified particle scope + * @param particleId Particle ID to remove handlers for + */ + removeParticleScopeHandlers(particleId: string): void { + this.particleScopeHandlers.delete(particleId); + } + + /** + * Find call service handler for specified particle + * @param serviceId Service ID as specified in `call` air instruction + * @param fnName Function name as specified in `call` air instruction + * @param particleId Particle ID + */ + getHandler(serviceId: string, fnName: string, particleId: string): GenericCallServiceHandler | null { + const key = serviceFnKey(serviceId, fnName); + const psh = this.particleScopeHandlers.get(particleId); + let handler: GenericCallServiceHandler | undefined = undefined; + + // we should prioritize handler for this particle if there is one + // if particle-scoped handler exist for this particle try getting handler there + if (psh !== undefined) { + handler = psh.get(key); + } + + // then try to find a common handler for all particles with this service-fn key + // if there is no particle-specific handler, get one from common map + if (handler === undefined) { + handler = this.commonHandlers.get(key); + } + + return handler || null; + } + + /** + * Execute service call for specified call service data. Return null if no handler was found + */ + async callService(req: CallServiceData): Promise { + const handler = this.getHandler(req.serviceId, req.fnName, req.particleContext.particleId); + + if (handler === null) { + return null; + } + + const result = await handler(req); + + // Otherwise AVM might break + if (result.result === undefined) { + result.result = null; + } + + return result; + } + + /** + * Register handler for all particles + */ + registerGlobalHandler(serviceId: string, fnName: string, handler: GenericCallServiceHandler): void { + this.commonHandlers.set(serviceFnKey(serviceId, fnName), handler); + } + + /** + * Register handler which will be called only for particle with the specific id + */ + registerParticleScopeHandler( + particleId: string, + serviceId: string, + fnName: string, + handler: GenericCallServiceHandler, + ): void { + let psh = this.particleScopeHandlers.get(particleId); + if (psh === undefined) { + psh = new Map(); + this.particleScopeHandlers.set(particleId, psh); + } + + psh.set(serviceFnKey(serviceId, fnName), handler); + } +} + +function serviceFnKey(serviceId: string, fnName: string) { + return `${serviceId}/${fnName}`; +} diff --git a/packages/core/js-peer/src/interfaces/commonTypes.ts b/packages/core/js-peer/src/jsServiceHost/interfaces.ts similarity index 61% rename from packages/core/js-peer/src/interfaces/commonTypes.ts rename to packages/core/js-peer/src/jsServiceHost/interfaces.ts index 878337c6..ada283e7 100644 --- a/packages/core/js-peer/src/interfaces/commonTypes.ts +++ b/packages/core/js-peer/src/jsServiceHost/interfaces.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 Fluence Labs Limited + * Copyright 2023 Fluence Labs Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +13,54 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import type { PeerIdB58 } from '@fluencelabs/interfaces'; import type { SecurityTetraplet } from '@fluencelabs/avm'; +import { JSONValue } from '../util/commonTypes.js'; + +/** + * JS Service host a low level interface for managing pure javascript services. + * It operates on a notion of Call Service Handlers - functions which are called when a `call` air instruction is executed on the local peer. + */ +export interface IJsServiceHost { + /** + * Returns true if any handler for the specified serviceId is registered + */ + hasService(serviceId: string): boolean; + + /** + * Find call service handler for specified particle + * @param serviceId Service ID as specified in `call` air instruction + * @param fnName Function name as specified in `call` air instruction + * @param particleId Particle ID + */ + getHandler(serviceId: string, fnName: string, particleId: string): GenericCallServiceHandler | null; + + /** + * Execute service call for specified call service data + */ + callService(req: CallServiceData): Promise; + + /** + * Register handler for all particles + */ + registerGlobalHandler(serviceId: string, fnName: string, handler: GenericCallServiceHandler): void; + + /** + * Register handler which will be called only for particle with the specific id + */ + registerParticleScopeHandler( + particleId: string, + serviceId: string, + fnName: string, + handler: GenericCallServiceHandler, + ): void; + + /** + * Removes all handlers associated with the specified particle scope + * @param particleId Particle ID to remove handlers for + */ + removeParticleScopeHandlers(particleId: string): void; +} export enum ResultCodes { success = 0, @@ -106,7 +151,3 @@ export interface CallServiceResult { */ result: CallServiceResultType; } - -export type JSONValue = string | number | boolean | null | { [x: string]: JSONValue } | Array; -export type JSONArray = Array; -export type JSONObject = { [x: string]: JSONValue }; diff --git a/packages/core/js-peer/src/jsServiceHost/serviceUtils.ts b/packages/core/js-peer/src/jsServiceHost/serviceUtils.ts new file mode 100644 index 00000000..234fbf09 --- /dev/null +++ b/packages/core/js-peer/src/jsServiceHost/serviceUtils.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { FluencePeer } from '../jsPeer/FluencePeer.js'; +import { IParticle } from '../particle/interfaces.js'; +import { builtInServices } from '../services/builtins.js'; +import { + CallServiceData, + CallServiceResult, + CallServiceResultType, + ParticleContext, + ResultCodes, +} from './interfaces.js'; + +export const doNothing = (..._args: Array) => undefined; + +export const WrapFnIntoServiceCall = + (fn: (args: any[]) => CallServiceResultType) => + (req: CallServiceData): CallServiceResult => ({ + retCode: ResultCodes.success, + result: fn(req.args), + }); + +export class ServiceError extends Error { + constructor(message: string) { + super(message); + + Object.setPrototypeOf(this, ServiceError.prototype); + } +} + +export const getParticleContext = (particle: IParticle): ParticleContext => { + return { + particleId: particle.id, + initPeerId: particle.initPeerId, + timestamp: particle.timestamp, + ttl: particle.ttl, + signature: particle.signature, + }; +}; + +export function registerDefaultServices(peer: FluencePeer) { + Object.entries(builtInServices).forEach(([serviceId, service]) => { + Object.entries(service).forEach(([fnName, fn]) => { + peer.internals.regHandler.common(serviceId, fnName, fn); + }); + }); +} diff --git a/packages/core/js-peer/src/js-peer/__test__/integration/marine-js.spec.ts b/packages/core/js-peer/src/marine/__test__/marine-js.spec.ts similarity index 80% rename from packages/core/js-peer/src/js-peer/__test__/integration/marine-js.spec.ts rename to packages/core/js-peer/src/marine/__test__/marine-js.spec.ts index f9ad801c..22ecc6e0 100644 --- a/packages/core/js-peer/src/js-peer/__test__/integration/marine-js.spec.ts +++ b/packages/core/js-peer/src/marine/__test__/marine-js.spec.ts @@ -3,14 +3,14 @@ import { it, describe, expect, beforeAll } from 'vitest'; import * as fs from 'fs'; import * as url from 'url'; import * as path from 'path'; -import { compileAqua, withPeer } from '../util.js'; +import { compileAqua, withPeer } from '../../util/testUtils.js'; let aqua: any; const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); describe('Marine js tests', () => { beforeAll(async () => { - const pathToAquaFiles = path.join(__dirname, '../../../../aqua_test/marine-js.aqua'); + const pathToAquaFiles = path.join(__dirname, '../../../aqua_test/marine-js.aqua'); const { services, functions } = await compileAqua(pathToAquaFiles); aqua = functions; }); @@ -18,7 +18,7 @@ describe('Marine js tests', () => { it('should call marine service correctly', async () => { await withPeer(async (peer) => { // arrange - const wasm = await fs.promises.readFile(path.join(__dirname, '../data/greeting.wasm')); + const wasm = await fs.promises.readFile(path.join(__dirname, '../../../data_for_test/greeting.wasm')); await peer.registerMarineService(wasm, 'greeting'); // act diff --git a/packages/core/js-peer/src/marine/deps-loader/common.ts b/packages/core/js-peer/src/marine/deps-loader/common.ts index c290ad45..df7ba986 100644 --- a/packages/core/js-peer/src/marine/deps-loader/common.ts +++ b/packages/core/js-peer/src/marine/deps-loader/common.ts @@ -1,10 +1,25 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ // @ts-ignore import { BlobWorker } from 'threads'; import { fromBase64, toUint8Array } from 'js-base64'; // @ts-ignore import type { WorkerImplementation } from 'threads/dist/types/master'; -import { LazyLoader } from '../../interfaces/index.js'; import { Buffer } from 'buffer'; +import { LazyLoader } from '../interfaces.js'; export class InlinedWorkerLoader extends LazyLoader { constructor(b64script: string) { diff --git a/packages/core/js-peer/src/marine/deps-loader/node.ts b/packages/core/js-peer/src/marine/deps-loader/node.ts index 38b68c47..79f0a647 100644 --- a/packages/core/js-peer/src/marine/deps-loader/node.ts +++ b/packages/core/js-peer/src/marine/deps-loader/node.ts @@ -1,5 +1,19 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import { createRequire } from 'module'; -import { LazyLoader } from '../../interfaces/index.js'; // @ts-ignore import type { WorkerImplementation } from 'threads/dist/types/master'; @@ -8,6 +22,7 @@ import { Worker } from 'threads'; import { Buffer } from 'buffer'; import * as fs from 'fs'; import * as path from 'path'; +import { LazyLoader } from '../interfaces.js'; const require = createRequire(import.meta.url); diff --git a/packages/core/js-peer/src/marine/deps-loader/web.ts b/packages/core/js-peer/src/marine/deps-loader/web.ts index 6beb6162..3e4d91fa 100644 --- a/packages/core/js-peer/src/marine/deps-loader/web.ts +++ b/packages/core/js-peer/src/marine/deps-loader/web.ts @@ -1,5 +1,22 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import { Buffer } from 'buffer'; -import { LazyLoader } from '../../interfaces/index.js'; +import { LazyLoader } from '../interfaces.js'; +// @ts-ignore +import type { WorkerImplementation } from 'threads/dist/types/master'; const bufferToSharedArrayBuffer = (buffer: Buffer): SharedArrayBuffer => { const sab = new SharedArrayBuffer(buffer.length); @@ -17,7 +34,7 @@ const bufferToSharedArrayBuffer = (buffer: Buffer): SharedArrayBuffer => { * @param filePath - path to the wasm file relative to current origin * @returns Either SharedArrayBuffer or Buffer with the wasm file */ -export const loadWasmFromServer = async (filePath: string): Promise => { +export const loadWasmFromUrl = async (filePath: string): Promise => { const fullUrl = window.location.origin + '/' + filePath; const res = await fetch(fullUrl); const ab = await res.arrayBuffer(); @@ -33,8 +50,14 @@ export const loadWasmFromServer = async (filePath: string): Promise { +export class WasmLoaderFromUrl extends LazyLoader { constructor(filePath: string) { - super(() => loadWasmFromServer(filePath)); + super(() => loadWasmFromUrl(filePath)); + } +} + +export class WorkerLoaderFromUrl extends LazyLoader { + constructor(scriptPath: string) { + super(() => new Worker(scriptPath)); } } diff --git a/packages/core/js-peer/src/interfaces/index.ts b/packages/core/js-peer/src/marine/interfaces.ts similarity index 55% rename from packages/core/js-peer/src/interfaces/index.ts rename to packages/core/js-peer/src/marine/interfaces.ts index 1617f513..3789ed8f 100644 --- a/packages/core/js-peer/src/interfaces/index.ts +++ b/packages/core/js-peer/src/marine/interfaces.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 Fluence Labs Limited + * Copyright 2023 Fluence Labs Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,28 +13,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import type { PeerIdB58 } from '@fluencelabs/interfaces'; -import type { JSONArray, JSONObject, LogLevel } from '@fluencelabs/marine-js/dist/types'; -import type { RunParameters, CallResultsArray, InterpreterResult } from '@fluencelabs/avm'; +import { CallResultsArray, InterpreterResult, RunParameters } from '@fluencelabs/avm'; +import { IStartable, JSONArray, JSONObject } from '../util/commonTypes.js'; +import { Buffer } from 'buffer'; // @ts-ignore import type { WorkerImplementation } from 'threads/dist/types/master'; -export type ParticleHandler = (particle: string) => void; - /** - * Base class for connectivity layer to Fluence Network + * Contract for marine host implementations. Marine host is responsible for creating calling and removing marine services */ -export abstract class FluenceConnection { - abstract readonly relayPeerId: PeerIdB58 | null; - abstract connect(onIncomingParticle: ParticleHandler): Promise; - abstract disconnect(): Promise; - abstract sendParticle(nextPeerIds: PeerIdB58[], particle: string): Promise; -} - -export interface IMarine extends IModule { +export interface IMarineHost extends IStartable { + /** + * Creates marine service from the given module and service id + */ createService(serviceModule: SharedArrayBuffer | Buffer, serviceId: string): Promise; + /** + * Removes marine service with the given service id + */ + removeService(serviceId: string): void; + + /** + * Returns true if any service with the specified service id is registered + */ + hasService(serviceId: string): boolean; + + /** + * Calls the specified function of the specified service with the given arguments + */ callService( serviceId: string, functionName: string, @@ -43,7 +49,13 @@ export interface IMarine extends IModule { ): Promise; } -export interface IAvmRunner extends IModule { +/** + * Interface for different implementations of AVM runner + */ +export interface IAvmRunner extends IStartable { + /** + * Run AVM interpreter with the specified parameters + */ run( runParams: RunParameters, air: string, @@ -53,20 +65,27 @@ export interface IAvmRunner extends IModule { ): Promise; } -export interface IModule { - start(): Promise; - stop(): Promise; -} - +/** + * Interface for something which can hold a value + */ export interface IValueLoader { getValue(): T; } -export interface IWasmLoader extends IValueLoader, IModule {} +/** + * Interface for something which can load wasm files + */ +export interface IWasmLoader extends IValueLoader, IStartable {} -export interface IWorkerLoader extends IValueLoader, IModule {} +/** + * Interface for something which can thread.js based worker + */ +export interface IWorkerLoader extends IValueLoader, IStartable {} -export class LazyLoader implements IModule, IValueLoader { +/** + * Lazy loader for some value. Value is loaded only when `start` method is called + */ +export class LazyLoader implements IStartable, IValueLoader { private value: T | null = null; constructor(private loadValue: () => Promise | T) {} diff --git a/packages/core/js-peer/src/marine/worker-script/index.ts b/packages/core/js-peer/src/marine/worker-script/index.ts index 5f913ef9..8bfa6242 100644 --- a/packages/core/js-peer/src/marine/worker-script/index.ts +++ b/packages/core/js-peer/src/marine/worker-script/index.ts @@ -17,6 +17,7 @@ import { MarineService } from '@fluencelabs/marine-js/dist/MarineService'; import type { Env, MarineServiceConfig } from '@fluencelabs/marine-js/dist/config'; import type { JSONArray, JSONObject, LogMessage } from '@fluencelabs/marine-js/dist/types'; +import { Buffer } from 'buffer'; // @ts-ignore import { Observable, Subject } from 'threads/observable'; // @ts-ignore diff --git a/packages/core/js-peer/src/marine/worker-script/workerLoader.ts b/packages/core/js-peer/src/marine/worker-script/workerLoader.ts index 5c26226d..3ae67d13 100644 --- a/packages/core/js-peer/src/marine/worker-script/workerLoader.ts +++ b/packages/core/js-peer/src/marine/worker-script/workerLoader.ts @@ -1,9 +1,23 @@ -import { LazyLoader } from '../../interfaces/index.js'; - +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ // @ts-ignore import type { WorkerImplementation } from 'threads/dist/types/master'; // @ts-ignore import { Worker } from 'threads'; +import { LazyLoader } from '../interfaces.js'; export class WorkerLoader extends LazyLoader { constructor() { diff --git a/packages/core/js-peer/src/marine/worker/index.ts b/packages/core/js-peer/src/marine/worker/index.ts index 4ac9cf9e..f793226c 100644 --- a/packages/core/js-peer/src/marine/worker/index.ts +++ b/packages/core/js-peer/src/marine/worker/index.ts @@ -14,29 +14,40 @@ * limitations under the License. */ -import type { JSONArray, JSONObject, LogLevel } from '@fluencelabs/marine-js/dist/types'; +import type { JSONArray, JSONObject } from '@fluencelabs/marine-js/dist/types'; import { LogFunction, logLevelToEnv } from '@fluencelabs/marine-js/dist/types'; -import type { IMarine, IWorkerLoader, IWasmLoader } from '../../interfaces/index.js'; import type { MarineBackgroundInterface } from '../worker-script/index.js'; // @ts-ignore import { spawn, Thread } from 'threads'; // @ts-ignore import type { ModuleThread } from 'threads'; +import { Buffer } from 'buffer'; import { MarineLogger, marineLogger } from '../../util/logger.js'; +import { IMarineHost, IWasmLoader, IWorkerLoader } from '../interfaces.js'; -export class MarineBackgroundRunner implements IMarine { +export class MarineBackgroundRunner implements IMarineHost { + private marineServices = new Set(); private workerThread?: ModuleThread; private loggers: Map = new Map(); constructor(private workerLoader: IWorkerLoader, private controlModuleLoader: IWasmLoader) {} + hasService(serviceId: string): boolean { + return this.marineServices.has(serviceId); + } + + removeService(serviceId: string): void { + this.marineServices.delete(serviceId); + } + async start(): Promise { if (this.workerThread) { return; } + this.marineServices = new Set(); await this.workerLoader.start(); await this.controlModuleLoader.start(); const worker = this.workerLoader.getValue(); @@ -53,7 +64,7 @@ export class MarineBackgroundRunner implements IMarine { await this.workerThread.init(wasm); } - createService(serviceModule: SharedArrayBuffer | Buffer, serviceId: string): Promise { + async createService(serviceModule: SharedArrayBuffer | Buffer, serviceId: string): Promise { if (!this.workerThread) { throw 'Worker is not initialized'; } @@ -62,7 +73,8 @@ export class MarineBackgroundRunner implements IMarine { // We enable all possible log levels passing the control for exact printouts to the logger const env = logLevelToEnv('trace'); this.loggers.set(serviceId, marineLogger(serviceId)); - return this.workerThread.createService(serviceModule, serviceId, undefined, env); + await this.workerThread.createService(serviceModule, serviceId, undefined, env); + this.marineServices.add(serviceId); } callService( @@ -83,6 +95,7 @@ export class MarineBackgroundRunner implements IMarine { return; } + this.marineServices.clear(); await this.workerThread.terminate(); await Thread.terminate(this.workerThread); } diff --git a/packages/core/js-peer/src/particle/Particle.ts b/packages/core/js-peer/src/particle/Particle.ts new file mode 100644 index 00000000..c44c3b7e --- /dev/null +++ b/packages/core/js-peer/src/particle/Particle.ts @@ -0,0 +1,129 @@ +/* + * Copyright 2020 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { fromUint8Array, toUint8Array } from 'js-base64'; +import { CallResultsArray } from '@fluencelabs/avm'; +import { v4 as uuidv4 } from 'uuid'; +import { Buffer } from 'buffer'; +import { IParticle } from './interfaces.js'; + +export class Particle implements IParticle { + readonly signature: undefined; + + constructor( + public readonly id: string, + public readonly timestamp: number, + public readonly script: string, + public readonly data: Uint8Array, + public readonly ttl: number, + public readonly initPeerId: string, + ) { + this.signature = undefined; + } + + static createNew(script: string, initPeerId: string, ttl: number): Particle { + return new Particle(uuidv4(), Date.now(), script, Buffer.from([]), ttl, initPeerId); + } + + static fromString(str: string): Particle { + const json = JSON.parse(str); + const res = new Particle( + json.id, + json.timestamp, + json.script, + toUint8Array(json.data), + json.ttl, + json.init_peer_id, + ); + + return res; + } +} + +/** + * Returns actual ttl of a particle, i.e. ttl - time passed since particle creation + */ +export const getActualTTL = (particle: IParticle): number => { + return particle.timestamp + particle.ttl - Date.now(); +}; + +/** + * Returns true if particle has expired + */ +export const hasExpired = (particle: IParticle): boolean => { + return getActualTTL(particle) <= 0; +}; + +/** + * Creates a particle clone with new data + */ +export const cloneWithNewData = (particle: IParticle, newData: Uint8Array): IParticle => { + return new Particle(particle.id, particle.timestamp, particle.script, newData, particle.ttl, particle.initPeerId); +}; + +/** + * Creates a deep copy of a particle + */ +export const fullClone = (particle: IParticle): IParticle => { + return JSON.parse(JSON.stringify(particle)); +}; + +/** + * Serializes particle into string suitable for sending through network + */ +export const serializeToString = (particle: IParticle): string => { + return JSON.stringify({ + action: 'Particle', + id: particle.id, + init_peer_id: particle.initPeerId, + timestamp: particle.timestamp, + ttl: particle.ttl, + script: particle.script, + // TODO: copy signature from a particle after signatures will be implemented on nodes + signature: [], + data: particle.data && fromUint8Array(particle.data), + }); +}; + +/** + * When particle is executed, it goes through different stages. The type describes all possible stages and their parameters + */ +export type ParticleExecutionStage = + | { stage: 'received' } + | { stage: 'interpreted' } + | { stage: 'interpreterError'; errorMessage: string } + | { stage: 'localWorkDone' } + | { stage: 'sent' } + | { stage: 'sendingError'; errorMessage: string } + | { stage: 'expired' }; + +/** + * Particle queue item is a wrapper around particle, which contains additional information about particle execution + */ +export interface ParticleQueueItem { + particle: IParticle; + callResults: CallResultsArray; + onStageChange: (state: ParticleExecutionStage) => void; +} + +/** + * Helper function to handle particle at expired stage + */ +export const handleTimeout = (fn: () => void) => (stage: ParticleExecutionStage) => { + if (stage.stage === 'expired') { + fn(); + } +}; diff --git a/packages/core/js-peer/src/particle/interfaces.ts b/packages/core/js-peer/src/particle/interfaces.ts new file mode 100644 index 00000000..551092e2 --- /dev/null +++ b/packages/core/js-peer/src/particle/interfaces.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { PeerIdB58 } from '@fluencelabs/interfaces'; + +/** + * Immutable part of the particle. + */ +export interface IImmutableParticlePart { + /** + * Particle id + */ + readonly id: string; + + /** + * Particle timestamp. Specifies when the particle was created. + */ + readonly timestamp: number; + + /** + * Particle's air script + */ + readonly script: string; + + /** + * Particle's ttl. Specifies how long the particle is valid in milliseconds. + */ + readonly ttl: number; + + /** + * Peer id where the particle was initiated. + */ + readonly initPeerId: PeerIdB58; + + // TODO: implement particle signatures + readonly signature: undefined; +} + +/** + * Particle is a data structure that is used to transfer data between peers in Fluence network. + */ +export interface IParticle extends IImmutableParticlePart { + /** + * Mutable particle data + */ + data: Uint8Array; +} diff --git a/packages/core/js-peer/src/services/NodeUtils.ts b/packages/core/js-peer/src/services/NodeUtils.ts new file mode 100644 index 00000000..98abed75 --- /dev/null +++ b/packages/core/js-peer/src/services/NodeUtils.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CallParams, IFluenceInternalApi } from '@fluencelabs/interfaces'; +import { defaultGuard } from './SingleModuleSrv.js'; +import { NodeUtilsDef, registerNodeUtils } from './_aqua/node-utils.js'; +import { SecurityGuard } from './securityGuard.js'; +import { readFile } from 'fs/promises'; +import { FluencePeer } from '../jsPeer/FluencePeer.js'; + +export class NodeUtils implements NodeUtilsDef { + constructor(private peer: FluencePeer) { + this.securityGuard_readFile = defaultGuard(this.peer); + } + + securityGuard_readFile: SecurityGuard<'path'>; + + async read_file(path: string, callParams: CallParams<'path'>) { + if (!this.securityGuard_readFile(callParams)) { + return { + success: false, + error: 'Security guard validation failed', + content: null, + }; + } + + try { + // Strange enough, but Buffer type works here, while reading with encoding 'utf-8' doesn't + const data: any = await readFile(path); + return { + success: true, + content: data, + error: null, + }; + } catch (err: any) { + return { + success: false, + error: err.message, + content: null, + }; + } + } +} + +// HACK:: security guard functions must be ported to user API +export const doRegisterNodeUtils = (peer: any) => { + registerNodeUtils(peer, 'node_utils', new NodeUtils(peer)); +}; diff --git a/packages/core/js-peer/src/js-peer/builtins/Sig.ts b/packages/core/js-peer/src/services/Sig.ts similarity index 61% rename from packages/core/js-peer/src/js-peer/builtins/Sig.ts rename to packages/core/js-peer/src/services/Sig.ts index b3a90c8b..20b99441 100644 --- a/packages/core/js-peer/src/js-peer/builtins/Sig.ts +++ b/packages/core/js-peer/src/services/Sig.ts @@ -1,6 +1,23 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { CallParams, PeerIdB58 } from '@fluencelabs/interfaces'; -import { KeyPair } from '../../keypair/index.js'; -import { SigDef } from '../_aqua/services.js'; +import { KeyPair } from '../keypair/index.js'; +import { FluencePeer } from '../jsPeer/FluencePeer.js'; +import { SigDef } from './_aqua/services.js'; import { allowOnlyParticleOriginatedAt, allowServiceFn, and, or, SecurityGuard } from './securityGuard.js'; export const defaultSigGuard = (peerId: PeerIdB58) => { @@ -18,11 +35,7 @@ export const defaultSigGuard = (peerId: PeerIdB58) => { }; export class Sig implements SigDef { - private _keyPair: KeyPair; - - constructor(keyPair: KeyPair) { - this._keyPair = keyPair; - } + constructor(private keyPair: KeyPair) {} /** * Configurable security guard for sign method @@ -35,7 +48,7 @@ export class Sig implements SigDef { * Gets the public key of KeyPair. Required by aqua */ get_peer_id() { - return this._keyPair.getPeerId(); + return this.keyPair.getPeerId(); } /** @@ -53,7 +66,7 @@ export class Sig implements SigDef { }; } - const signedData = await this._keyPair.signBytes(Uint8Array.from(data)); + const signedData = await this.keyPair.signBytes(Uint8Array.from(data)); return { success: true, @@ -66,6 +79,10 @@ export class Sig implements SigDef { * Verifies the signature. Required by aqua */ verify(signature: number[], data: number[]): Promise { - return this._keyPair.verify(Uint8Array.from(data), Uint8Array.from(signature)); + return this.keyPair.verify(Uint8Array.from(data), Uint8Array.from(signature)); } } + +export const getDefaultSig = (peer: FluencePeer) => { + peer.registerMarineService; +}; diff --git a/packages/core/js-peer/src/js-peer/builtins/SingleModuleSrv.ts b/packages/core/js-peer/src/services/SingleModuleSrv.ts similarity index 54% rename from packages/core/js-peer/src/js-peer/builtins/SingleModuleSrv.ts rename to packages/core/js-peer/src/services/SingleModuleSrv.ts index 1eccb7dc..ace7fcf3 100644 --- a/packages/core/js-peer/src/js-peer/builtins/SingleModuleSrv.ts +++ b/packages/core/js-peer/src/services/SingleModuleSrv.ts @@ -1,13 +1,28 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { v4 as uuidv4 } from 'uuid'; -import { SrvDef } from '../_aqua/single-module-srv.js'; -import { NodeUtilsDef } from '../_aqua/node-utils.js'; -import { FluencePeer } from '../FluencePeer.js'; +import { SrvDef } from './_aqua/single-module-srv.js'; +import { FluencePeer } from '../jsPeer/FluencePeer.js'; import { CallParams } from '@fluencelabs/interfaces'; import { Buffer } from 'buffer'; import { allowOnlyParticleOriginatedAt, SecurityGuard } from './securityGuard.js'; export const defaultGuard = (peer: FluencePeer) => { - return allowOnlyParticleOriginatedAt(peer.getStatus().peerId!); + return allowOnlyParticleOriginatedAt(peer.keyPair.getPeerId()); }; export class Srv implements SrvDef { @@ -32,10 +47,11 @@ export class Srv implements SrvDef { try { const newServiceId = uuidv4(); const buffer = Buffer.from(wasm_b64_content, 'base64'); - const sab = new SharedArrayBuffer(buffer.length); - const tmp = new Uint8Array(sab); - tmp.set(buffer, 0); - await this.peer.registerMarineService(sab, newServiceId); + // TODO:: figure out why SharedArrayBuffer is not working here + // const sab = new SharedArrayBuffer(buffer.length); + // const tmp = new Uint8Array(sab); + // tmp.set(buffer, 0); + await this.peer.registerMarineService(buffer, newServiceId); this.services.add(newServiceId); return { @@ -83,49 +99,3 @@ export class Srv implements SrvDef { return Array.from(this.services.values()); } } - -export class NodeUtils implements NodeUtilsDef { - constructor(private peer: FluencePeer) { - this.securityGuard_readFile = defaultGuard(this.peer); - } - - securityGuard_readFile: SecurityGuard<'path'>; - - async read_file(path: string, callParams: CallParams<'path'>) { - // TODO: split node-only and universal services into different client packages - // if (!isNode) { - // return { - // success: false, - // error: 'read_file is only supported in node.js', - // content: null, - // }; - // } - - if (!this.securityGuard_readFile(callParams)) { - return { - success: false, - error: 'Security guard validation failed', - content: null, - }; - } - - try { - // eval('require') is needed so that - // webpack will complain about missing dependencies for web target - const r = eval('require'); - const fs = r('fs').promises; - const data = await fs.readFile(path); - return { - success: true, - content: data, - error: null, - }; - } catch (err: any) { - return { - success: false, - error: err.message, - content: null, - }; - } - } -} diff --git a/packages/core/js-peer/src/js-peer/__test__/unit/builtInHandler.spec.ts b/packages/core/js-peer/src/services/__test__/builtInHandler.spec.ts similarity index 98% rename from packages/core/js-peer/src/js-peer/__test__/unit/builtInHandler.spec.ts rename to packages/core/js-peer/src/services/__test__/builtInHandler.spec.ts index f4259dee..9516aaec 100644 --- a/packages/core/js-peer/src/js-peer/__test__/unit/builtInHandler.spec.ts +++ b/packages/core/js-peer/src/services/__test__/builtInHandler.spec.ts @@ -2,11 +2,11 @@ import { it, describe, expect, test } from 'vitest'; import { CallParams } from '@fluencelabs/interfaces'; import { toUint8Array } from 'js-base64'; -import { CallServiceData } from '../../../interfaces/commonTypes.js'; -import { KeyPair } from '../../../keypair/index.js'; -import { Sig, defaultSigGuard } from '../../builtins/Sig.js'; -import { allowServiceFn } from '../../builtins/securityGuard.js'; -import { builtInServices } from '../../builtins/common.js'; +import { KeyPair } from '../../keypair/index.js'; +import { Sig, defaultSigGuard } from '../Sig.js'; +import { allowServiceFn } from '../securityGuard.js'; +import { builtInServices } from '../builtins.js'; +import { CallServiceData } from '../../jsServiceHost/interfaces.js'; const a10b20 = `{ "a": 10, diff --git a/packages/core/js-peer/src/js-peer/__test__/integration/jsonBuiltin.spec.ts b/packages/core/js-peer/src/services/__test__/jsonBuiltin.spec.ts similarity index 89% rename from packages/core/js-peer/src/js-peer/__test__/integration/jsonBuiltin.spec.ts rename to packages/core/js-peer/src/services/__test__/jsonBuiltin.spec.ts index 1748526f..c13aab98 100644 --- a/packages/core/js-peer/src/js-peer/__test__/integration/jsonBuiltin.spec.ts +++ b/packages/core/js-peer/src/services/__test__/jsonBuiltin.spec.ts @@ -1,9 +1,9 @@ import { it, describe, expect, beforeEach, afterEach } from 'vitest'; -import { Particle } from '../../Particle.js'; -import { doNothing } from '../../utils.js'; -import { FluencePeer } from '../../FluencePeer.js'; -import { mkTestPeer } from '../util.js'; +import { Particle } from '../../particle/Particle.js'; +import { FluencePeer } from '../../jsPeer/FluencePeer.js'; +import { mkTestPeer } from '../../util/testUtils.js'; +import { doNothing } from '../../jsServiceHost/serviceUtils.js'; let peer: FluencePeer; @@ -15,7 +15,7 @@ describe('Sig service test suite', () => { }); beforeEach(async () => { - peer = mkTestPeer(); + peer = await mkTestPeer(); await peer.start(); }); @@ -56,7 +56,7 @@ describe('Sig service test suite', () => { }; }); }); - const p = peer.internals.createNewParticle(script) as Particle; + const p = peer.internals.createNewParticle(script); await peer.internals.initiateParticle(p, doNothing); const [nestedFirst, nestedSecond, outerFirst, outerSecond, outerFirstString, outerFirstParsed] = await promise; diff --git a/packages/core/js-peer/src/js-peer/__test__/integration/sigService.spec.ts b/packages/core/js-peer/src/services/__test__/sigService.spec.ts similarity index 86% rename from packages/core/js-peer/src/js-peer/__test__/integration/sigService.spec.ts rename to packages/core/js-peer/src/services/__test__/sigService.spec.ts index 21d35c7b..c3a40b0f 100644 --- a/packages/core/js-peer/src/js-peer/__test__/integration/sigService.spec.ts +++ b/packages/core/js-peer/src/services/__test__/sigService.spec.ts @@ -2,11 +2,11 @@ import { it, describe, expect, beforeAll } from 'vitest'; import * as path from 'path'; import * as url from 'url'; -import { KeyPair } from '../../../keypair/index.js'; -import { allowServiceFn } from '../../builtins/securityGuard.js'; -import { Sig } from '../../builtins/Sig.js'; -import { compileAqua, withPeer } from '../util.js'; -import { registerService } from '../../../compilerSupport/registerService.js'; +import { KeyPair } from '../../keypair/index.js'; +import { allowServiceFn } from '../securityGuard.js'; +import { Sig } from '../Sig.js'; +import { registerService } from '../../compilerSupport/registerService.js'; +import { compileAqua, withPeer } from '../../util/testUtils.js'; const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); @@ -16,7 +16,7 @@ let dataProviderDef: any; describe('Sig service test suite', () => { beforeAll(async () => { - const pathToAquaFiles = path.join(__dirname, '../../../../aqua_test/sigService.aqua'); + const pathToAquaFiles = path.join(__dirname, '../../../aqua_test/sigService.aqua'); const { services, functions } = await compileAqua(pathToAquaFiles); aqua = functions; @@ -75,12 +75,13 @@ describe('Sig service test suite', () => { customSig.securityGuard = allowServiceFn('wrong', 'wrong'); const result = await aqua.callSig(peer, { sigId: 'CustomSig' }); + expect(result.success).toBe(false); }); }); it('Default sig service should be resolvable by peer id', async () => { await withPeer(async (peer) => { - const sig = peer.getServices().sig; + const sig = peer.internals.getServices().sig; const data = [1, 2, 3, 4, 5]; registerService({ @@ -95,7 +96,7 @@ describe('Sig service test suite', () => { }); const callAsSigRes = await aqua.callSig(peer, { sigId: 'sig' }); - const callAsPeerIdRes = await aqua.callSig(peer, { sigId: peer.getStatus().peerId }); + const callAsPeerIdRes = await aqua.callSig(peer, { sigId: peer.keyPair.getPeerId() }); expect(callAsSigRes.success).toBe(false); expect(callAsPeerIdRes.success).toBe(false); @@ -104,7 +105,7 @@ describe('Sig service test suite', () => { const callAsSigResAfterGuardChange = await aqua.callSig(peer, { sigId: 'sig' }); const callAsPeerIdResAfterGuardChange = await aqua.callSig(peer, { - sigId: peer.getStatus().peerId, + sigId: peer.keyPair.getPeerId(), }); expect(callAsSigResAfterGuardChange.success).toBe(true); diff --git a/packages/core/js-peer/src/js-peer/__test__/integration/srv.spec.ts b/packages/core/js-peer/src/services/__test__/srv.spec.ts similarity index 67% rename from packages/core/js-peer/src/js-peer/__test__/integration/srv.spec.ts rename to packages/core/js-peer/src/services/__test__/srv.spec.ts index b9676298..48c649c1 100644 --- a/packages/core/js-peer/src/js-peer/__test__/integration/srv.spec.ts +++ b/packages/core/js-peer/src/services/__test__/srv.spec.ts @@ -1,14 +1,16 @@ import { it, describe, expect, beforeAll } from 'vitest'; import * as path from 'path'; import * as url from 'url'; -import { compileAqua, withPeer } from '../util.js'; +import { compileAqua, withPeer } from '../../util/testUtils.js'; +import { registerNodeUtils } from '../_aqua/node-utils.js'; +import { NodeUtils } from '../NodeUtils.js'; const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); let aqua: any; describe('Srv service test suite', () => { beforeAll(async () => { - const pathToAquaFiles = path.join(__dirname, '../../../../aqua_test/srv.aqua'); + const pathToAquaFiles = path.join(__dirname, '../../../aqua_test/srv.aqua'); const { services, functions } = await compileAqua(pathToAquaFiles); aqua = functions; }); @@ -16,7 +18,8 @@ describe('Srv service test suite', () => { it('Use custom srv service, success path', async () => { await withPeer(async (peer) => { // arrange - const wasm = path.join(__dirname, '../data/greeting.wasm'); + registerNodeUtils(peer, 'node_utils', new NodeUtils(peer)); + const wasm = path.join(__dirname, '../../../data_for_test/greeting.wasm'); // act const res = await aqua.happy_path(peer, { file_path: wasm }); @@ -29,7 +32,8 @@ describe('Srv service test suite', () => { it('List deployed services', async () => { await withPeer(async (peer) => { // arrange - const wasm = path.join(__dirname, '../data/greeting.wasm'); + registerNodeUtils(peer, 'node_utils', new NodeUtils(peer)); + const wasm = path.join(__dirname, '../../../data_for_test/greeting.wasm'); // act const res = await aqua.list_services(peer, { file_path: wasm }); @@ -42,19 +46,21 @@ describe('Srv service test suite', () => { it('Correct error for removed services', async () => { await withPeer(async (peer) => { // arrange - const wasm = path.join(__dirname, '../data/greeting.wasm'); + registerNodeUtils(peer, 'node_utils', new NodeUtils(peer)); + const wasm = path.join(__dirname, '../../../data_for_test/greeting.wasm'); // act const res = await aqua.service_removed(peer, { file_path: wasm }); // assert - expect(res).toMatch('No handler has been registered for serviceId'); + expect(res).toMatch('No service found for service call'); }); }); it('Correct error for file not found', async () => { await withPeer(async (peer) => { // arrange + registerNodeUtils(peer, 'node_utils', new NodeUtils(peer)); // act const res = await aqua.file_not_found(peer, {}); @@ -67,6 +73,7 @@ describe('Srv service test suite', () => { it('Correct error for removing non existing service', async () => { await withPeer(async (peer) => { // arrange + registerNodeUtils(peer, 'node_utils', new NodeUtils(peer)); // act const res = await aqua.removing_non_exiting(peer, {}); diff --git a/packages/core/js-peer/src/js-peer/_aqua/node-utils.ts b/packages/core/js-peer/src/services/_aqua/node-utils.ts similarity index 87% rename from packages/core/js-peer/src/js-peer/_aqua/node-utils.ts rename to packages/core/js-peer/src/services/_aqua/node-utils.ts index 0208f436..407b7c3a 100644 --- a/packages/core/js-peer/src/js-peer/_aqua/node-utils.ts +++ b/packages/core/js-peer/src/services/_aqua/node-utils.ts @@ -6,9 +6,8 @@ * Aqua version: 0.7.7-362 * */ -import { CallParams } from '@fluencelabs/interfaces'; -import { registerServiceImpl } from './util.js'; -import { FluencePeer } from '../FluencePeer.js'; +import { CallParams, IFluenceInternalApi } from '@fluencelabs/interfaces'; +import { registerService } from '../../compilerSupport/registerService.js'; // Services @@ -21,10 +20,12 @@ export interface NodeUtilsDef { | Promise<{ content: string | null; error: string | null; success: boolean }>; } -export function registerNodeUtils(peer: FluencePeer, serviceId: string, service: any) { - registerServiceImpl( - peer, - { +export function registerNodeUtils(peer: IFluenceInternalApi, serviceId: string, service: any) { + registerService({ + peer: peer, + service: service, + serviceId: serviceId, + def: { defaultServiceId: 'node_utils', functions: { tag: 'labeledProduct', @@ -73,9 +74,7 @@ export function registerNodeUtils(peer: FluencePeer, serviceId: string, service: }, }, }, - serviceId, - service, - ); + }); } // Functions diff --git a/packages/core/js-peer/src/js-peer/_aqua/services.ts b/packages/core/js-peer/src/services/_aqua/services.ts similarity index 93% rename from packages/core/js-peer/src/js-peer/_aqua/services.ts rename to packages/core/js-peer/src/services/_aqua/services.ts index 33b3e961..d769b5a3 100644 --- a/packages/core/js-peer/src/js-peer/_aqua/services.ts +++ b/packages/core/js-peer/src/services/_aqua/services.ts @@ -6,9 +6,8 @@ * Aqua version: 0.7.7-362 * */ -import { CallParams } from '@fluencelabs/interfaces'; -import { registerServiceImpl } from './util.js'; -import { FluencePeer } from '../FluencePeer.js'; +import { CallParams, IFluenceInternalApi } from '@fluencelabs/interfaces'; +import { registerService } from '../../compilerSupport/registerService.js'; // Services @@ -27,10 +26,12 @@ export interface SigDef { ) => boolean | Promise; } -export function registerSig(peer: FluencePeer, serviceId: string, service: any) { - registerServiceImpl( - peer, - { +export function registerSig(peer: IFluenceInternalApi, serviceId: string, service: any) { + registerService({ + peer: peer as any, + service: service, + serviceId: serviceId, + def: { defaultServiceId: 'sig', functions: { tag: 'labeledProduct', @@ -131,9 +132,7 @@ export function registerSig(peer: FluencePeer, serviceId: string, service: any) }, }, }, - serviceId, - service, - ); + }); } // Functions diff --git a/packages/core/js-peer/src/js-peer/_aqua/single-module-srv.ts b/packages/core/js-peer/src/services/_aqua/single-module-srv.ts similarity index 94% rename from packages/core/js-peer/src/js-peer/_aqua/single-module-srv.ts rename to packages/core/js-peer/src/services/_aqua/single-module-srv.ts index f0824043..c28f8830 100644 --- a/packages/core/js-peer/src/js-peer/_aqua/single-module-srv.ts +++ b/packages/core/js-peer/src/services/_aqua/single-module-srv.ts @@ -6,9 +6,8 @@ * Aqua version: 0.7.7-362 * */ -import { CallParams } from '@fluencelabs/interfaces'; -import { registerServiceImpl } from './util.js'; -import { FluencePeer } from '../FluencePeer.js'; +import { CallParams, IFluenceInternalApi } from '@fluencelabs/interfaces'; +import { registerService } from '../../compilerSupport/registerService.js'; // Services @@ -26,10 +25,12 @@ export interface SrvDef { ) => { error: string | null; success: boolean } | Promise<{ error: string | null; success: boolean }>; } -export function registerSrv(peer: FluencePeer, serviceId: string, service: any) { - registerServiceImpl( - peer, - { +export function registerSrv(peer: IFluenceInternalApi, serviceId: string, service: any) { + registerService({ + peer: peer as any, + serviceId, + service, + def: { defaultServiceId: 'single_module_srv', functions: { tag: 'labeledProduct', @@ -130,9 +131,7 @@ export function registerSrv(peer: FluencePeer, serviceId: string, service: any) }, }, }, - serviceId, - service, - ); + }); } // Functions diff --git a/packages/core/js-peer/src/js-peer/builtins/common.ts b/packages/core/js-peer/src/services/builtins.ts similarity index 97% rename from packages/core/js-peer/src/js-peer/builtins/common.ts rename to packages/core/js-peer/src/services/builtins.ts index 8be42d7e..3aebdfd2 100644 --- a/packages/core/js-peer/src/js-peer/builtins/common.ts +++ b/packages/core/js-peer/src/services/builtins.ts @@ -19,9 +19,9 @@ import * as bs58 from 'bs58'; import { sha256 } from 'multiformats/hashes/sha2'; import { CallServiceResult } from '@fluencelabs/avm'; -import { GenericCallServiceHandler, ResultCodes } from '../../interfaces/commonTypes.js'; -import { jsonify } from '../utils.js'; +import { isString, jsonify } from '../util/utils.js'; import { Buffer } from 'buffer'; +import { GenericCallServiceHandler, ResultCodes } from '../jsServiceHost/interfaces.js'; //@ts-ignore const { encode, decode } = bs58.default; @@ -595,11 +595,3 @@ const checkForArgumentType = (req: { args: Array }, index: number, type return error(`Argument ${index} expected to be of type ${type}, Got ${actual}`); } }; - -export const isString = (unknown: unknown): unknown is string => { - return unknown !== null && typeof unknown === 'string'; -}; - -export const isObject = (unknown: unknown): unknown is object => { - return unknown !== null && typeof unknown === 'object'; -}; diff --git a/packages/core/js-peer/src/js-peer/builtins/securityGuard.ts b/packages/core/js-peer/src/services/securityGuard.ts similarity index 77% rename from packages/core/js-peer/src/js-peer/builtins/securityGuard.ts rename to packages/core/js-peer/src/services/securityGuard.ts index 0bc7e762..896505b8 100644 --- a/packages/core/js-peer/src/js-peer/builtins/securityGuard.ts +++ b/packages/core/js-peer/src/services/securityGuard.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { SecurityTetraplet } from '@fluencelabs/avm'; import { CallParams, PeerIdB58 } from '@fluencelabs/interfaces'; diff --git a/packages/core/js-peer/src/util/commonTypes.ts b/packages/core/js-peer/src/util/commonTypes.ts new file mode 100644 index 00000000..7a34d6a6 --- /dev/null +++ b/packages/core/js-peer/src/util/commonTypes.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface IStartable { + start(): Promise; + stop(): Promise; +} + +export type JSONValue = string | number | boolean | null | { [x: string]: JSONValue } | Array; +export type JSONArray = Array; +export type JSONObject = { [x: string]: JSONValue }; diff --git a/packages/core/js-peer/src/util/libp2pUtils.ts b/packages/core/js-peer/src/util/libp2pUtils.ts new file mode 100644 index 00000000..0cf84e7a --- /dev/null +++ b/packages/core/js-peer/src/util/libp2pUtils.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RelayOptions } from '@fluencelabs/interfaces'; +import { multiaddr, Multiaddr } from '@multiformats/multiaddr'; +import { isString } from './utils.js'; + +export function relayOptionToMultiaddr(relay: RelayOptions): Multiaddr { + const multiaddrString = isString(relay) ? relay : relay.multiaddr; + const ma = multiaddr(multiaddrString); + + throwIfHasNoPeerId(ma); + + return ma; +} + +export function throwIfHasNoPeerId(ma: Multiaddr): void { + const peerId = ma.getPeerId(); + if (!peerId) { + throw new Error('Specified multiaddr is invalid or missing peer id: ' + ma.toString()); + } +} diff --git a/packages/core/js-peer/src/util/logger.ts b/packages/core/js-peer/src/util/logger.ts index 7ae9bedb..5e05e3eb 100644 --- a/packages/core/js-peer/src/util/logger.ts +++ b/packages/core/js-peer/src/util/logger.ts @@ -1,5 +1,21 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import debug from 'debug'; -import { Particle } from '../js-peer/Particle.js'; +import { Buffer } from 'buffer'; // Format avm data as a string debug.formatters.a = (avmData: Uint8Array) => { diff --git a/packages/core/js-peer/src/util/testUtils.ts b/packages/core/js-peer/src/util/testUtils.ts new file mode 100644 index 00000000..0c49083b --- /dev/null +++ b/packages/core/js-peer/src/util/testUtils.ts @@ -0,0 +1,143 @@ +/* + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as api from '@fluencelabs/aqua-api/aqua-api.js'; + +import { promises as fs } from 'fs'; +import { DEFAULT_CONFIG, FluencePeer, PeerConfig } from '../jsPeer/FluencePeer.js'; +import { Particle } from '../particle/Particle.js'; +import { ClientConfig, IFluenceClient, RelayOptions, ServiceDef } from '@fluencelabs/interfaces'; +import { callAquaFunction } from '../compilerSupport/callFunction.js'; + +import { MarineBackgroundRunner } from '../marine/worker/index.js'; +import { MarineBasedAvmRunner } from '../jsPeer/avm.js'; +import { WorkerLoader } from '../marine/worker-script/workerLoader.js'; +import { KeyPair } from '../keypair/index.js'; +import { Subject, Subscribable } from 'rxjs'; +import { WrapFnIntoServiceCall } from '../jsServiceHost/serviceUtils.js'; +import { JsServiceHost } from '../jsServiceHost/JsServiceHost.js'; +import { ClientPeer, makeClientPeerConfig } from '../clientPeer/ClientPeer.js'; +import { WasmLoaderFromNpm } from '../marine/deps-loader/node.js'; +import { IConnection } from '../connection/interfaces.js'; + +export const registerHandlersHelper = ( + peer: FluencePeer, + particle: Particle, + handlers: Record>, +) => { + Object.entries(handlers).forEach(([serviceId, service]) => { + Object.entries(service).forEach(([fnName, fn]) => { + peer.internals.regHandler.forParticle(particle.id, serviceId, fnName, WrapFnIntoServiceCall(fn)); + }); + }); +}; + +export type CompiledFnCall = (peer: IFluenceClient, args: { [key: string]: any }) => Promise; +export type CompiledFile = { + functions: { [key: string]: CompiledFnCall }; + services: { [key: string]: ServiceDef }; +}; + +export const compileAqua = async (aquaFile: string): Promise => { + await fs.access(aquaFile); + + const compilationResult = await api.Aqua.compile(new api.Path(aquaFile), [], undefined); + + if (compilationResult.errors.length > 0) { + throw new Error('Aqua compilation failed. Error: ' + compilationResult.errors.join('/n')); + } + + const functions = Object.entries(compilationResult.functions) + .map(([name, fnInfo]) => { + const callFn = (peer: IFluenceClient, args: { [key: string]: any }) => { + return callAquaFunction({ + def: fnInfo.funcDef, + script: fnInfo.script, + config: {}, + peer: peer, + args, + }); + }; + return { [name]: callFn }; + }) + .reduce((agg, obj) => { + return { ...agg, ...obj }; + }, {}); + + return { functions, services: compilationResult.services }; +}; + +class NoopConnection implements IConnection { + getRelayPeerId(): string { + return 'nothing_here'; + } + supportsRelay(): boolean { + return true; + } + particleSource: Subscribable = new Subject(); + + sendParticle(nextPeerIds: string[], particle: Particle): Promise { + return Promise.resolve(); + } +} + +export class TestPeer extends FluencePeer { + constructor(keyPair: KeyPair, connection: IConnection) { + const workerLoader = new WorkerLoader(); + const controlModuleLoader = new WasmLoaderFromNpm('@fluencelabs/marine-js', 'marine-js.wasm'); + const avmModuleLoader = new WasmLoaderFromNpm('@fluencelabs/avm', 'avm.wasm'); + const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader); + const jsHost = new JsServiceHost(); + const avm = new MarineBasedAvmRunner(marine, avmModuleLoader); + super(DEFAULT_CONFIG, keyPair, marine, jsHost, avm, connection); + } +} + +export const mkTestPeer = async () => { + const kp = await KeyPair.randomEd25519(); + const conn = new NoopConnection(); + return new TestPeer(kp, conn); +}; + +export const withPeer = async (action: (p: FluencePeer) => Promise) => { + const p = await mkTestPeer(); + try { + await p.start(); + await action(p); + } finally { + await p.stop(); + } +}; + +export const withClient = async ( + relay: RelayOptions, + config: ClientConfig, + action: (client: ClientPeer) => Promise, +) => { + const workerLoader = new WorkerLoader(); + const controlModuleLoader = new WasmLoaderFromNpm('@fluencelabs/marine-js', 'marine-js.wasm'); + const avmModuleLoader = new WasmLoaderFromNpm('@fluencelabs/avm', 'avm.wasm'); + const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader); + const avm = new MarineBasedAvmRunner(marine, avmModuleLoader); + const { keyPair, peerConfig, relayConfig } = await makeClientPeerConfig(relay, config); + const client = new ClientPeer(peerConfig, relayConfig, keyPair, marine, avm); + try { + await client.connect(); + await action(client); + } finally { + await client.disconnect(); + } +}; diff --git a/packages/core/js-peer/src/util/utils.ts b/packages/core/js-peer/src/util/utils.ts new file mode 100644 index 00000000..3633b3a6 --- /dev/null +++ b/packages/core/js-peer/src/util/utils.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function jsonify(obj: unknown) { + return JSON.stringify(obj, null, 4); +} + +export const isString = (unknown: unknown): unknown is string => { + return unknown !== null && typeof unknown === 'string'; +}; + +export const isObject = (unknown: unknown): unknown is object => { + return unknown !== null && typeof unknown === 'object'; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32fe3894..9900abb5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,7 +35,7 @@ importers: base64-js: 1.5.1 devDependencies: '@fluencelabs/aqua-lib': 0.6.0 - '@fluencelabs/cli': 0.3.9_zefx7wxidm6hdha6gwewr46oqa + '@fluencelabs/cli': 0.3.9_mhciodunqktqftpx562wx5v3rq '@fluencelabs/registry': 0.8.2 '@fluencelabs/trust-graph': 3.1.2 @@ -123,21 +123,41 @@ importers: packages/client/js-client.node: specifiers: '@fluencelabs/avm': 0.35.4 + '@fluencelabs/interfaces': 0.7.4 '@fluencelabs/js-peer': 0.8.6 '@fluencelabs/marine-js': 0.3.45 '@types/platform': 1.3.4 platform: 1.3.6 dependencies: '@fluencelabs/avm': 0.35.4 + '@fluencelabs/interfaces': link:../../core/interfaces '@fluencelabs/js-peer': link:../../core/js-peer '@fluencelabs/marine-js': 0.3.45 platform: 1.3.6 devDependencies: '@types/platform': 1.3.4 + packages/client/js-client.web: + specifiers: + '@fluencelabs/interfaces': 0.7.4 + '@fluencelabs/js-peer': 0.8.6 + '@types/jest': 28.1.0 + '@types/node': 16.11.59 + jest: 28.1.0 + ts-jest: 28.0.2 + dependencies: + '@fluencelabs/interfaces': link:../../core/interfaces + '@fluencelabs/js-peer': link:../../core/js-peer + devDependencies: + '@types/jest': 28.1.0 + '@types/node': 16.11.59 + jest: 28.1.0_@types+node@16.11.59 + ts-jest: 28.0.2_m4pn7vsromlf5ffrouypoapnnq + packages/client/js-client.web.standalone: specifiers: '@fluencelabs/avm': 0.35.4 + '@fluencelabs/interfaces': 0.7.4 '@fluencelabs/js-peer': 0.8.6 '@fluencelabs/marine-js': 0.3.45 '@rollup/plugin-inject': 5.0.3 @@ -152,6 +172,7 @@ importers: vite-plugin-replace: 0.1.1 vite-tsconfig-paths: 4.0.3 dependencies: + '@fluencelabs/interfaces': link:../../core/interfaces '@fluencelabs/js-peer': link:../../core/js-peer buffer: 6.0.3 process: 0.11.10 @@ -163,10 +184,10 @@ importers: '@types/node': 16.11.59 jest: 28.1.0_@types+node@16.11.59 js-base64: 3.7.5 - ts-jest: 28.0.2_byf75w6xilfwy3ncjzlldwxox4 + ts-jest: 28.0.2_m4pn7vsromlf5ffrouypoapnnq vite: 4.0.4_@types+node@16.11.59 vite-plugin-replace: 0.1.1_vite@4.0.4 - vite-tsconfig-paths: 4.0.3_trrwuuiz4f5khno7hdf3cjz2ky + vite-tsconfig-paths: 4.0.3_egung5nfepmolqa7uavvqho3gq packages/client/tools: specifiers: @@ -256,7 +277,7 @@ importers: devDependencies: '@fluencelabs/aqua-api': 0.9.3 '@fluencelabs/aqua-lib': 0.6.0 - '@fluencelabs/cli': 0.3.9_g27jfiaq4eni6se3ukepjsbfru + '@fluencelabs/cli': 0.3.9_qpxy5lgkhl6krmgytbtmcegdtu '@fluencelabs/fluence-network-environment': 1.0.13 '@types/bs58': 4.0.1 '@types/debug': 4.1.7 @@ -2432,23 +2453,23 @@ packages: /@fluencelabs/avm/0.35.4: resolution: {integrity: sha512-J070t5AOYIzQnNcBcYjDPUDzJTcpVboZxcrjGN4qYiOjcrtCtnnXeQKedLuBto5bRztHJdL9BzLLvzcFXhgmFQ==} - /@fluencelabs/cli/0.3.9_g27jfiaq4eni6se3ukepjsbfru: + /@fluencelabs/cli/0.3.9_mhciodunqktqftpx562wx5v3rq: resolution: {integrity: sha512-xJYi7+AHrWt6RgWnr7Efr8Jpv0dNLoWhiCMvgSbXoFpIZzQAyNDgk5hnCdwIQ/eiJcNg0GHY0gyC+Q/d5YKc1Q==} engines: {node: '=16'} hasBin: true dependencies: '@fluencelabs/aqua-api': 0.10.3 - '@fluencelabs/deal-aurora': 0.1.8_oxd5x4wivhg5zjyucvh2su6sbi - '@fluencelabs/fluence': 0.28.0_p34j5hgwlk5eyxias25f7ntdym + '@fluencelabs/deal-aurora': 0.1.8_gc7gatgk3jrcbnir3tz2izmvqi + '@fluencelabs/fluence': 0.28.0_zchmlqqvawdfhvpuz6nrqhtseq '@fluencelabs/fluence-network-environment': 1.0.14 '@iarna/toml': 2.2.5 '@mswjs/interceptors': 0.19.5 '@oclif/color': 1.0.4 - '@oclif/core': 2.6.2_j777nnsruz44drbtesvg2fqc7y - '@oclif/plugin-autocomplete': 2.1.4_j777nnsruz44drbtesvg2fqc7y - '@oclif/plugin-help': 5.2.7_j777nnsruz44drbtesvg2fqc7y - '@oclif/plugin-not-found': 2.3.21_j777nnsruz44drbtesvg2fqc7y - '@walletconnect/universal-provider': 2.4.7_6ceuysb5murbu27s24v75mmoaq + '@oclif/core': 2.6.2_4bewfcp2iebiwuold25d6rgcsy + '@oclif/plugin-autocomplete': 2.1.4_4bewfcp2iebiwuold25d6rgcsy + '@oclif/plugin-help': 5.2.7_4bewfcp2iebiwuold25d6rgcsy + '@oclif/plugin-not-found': 2.3.21_4bewfcp2iebiwuold25d6rgcsy + '@walletconnect/universal-provider': 2.4.7_mqijgjbnahaa3tnbkktxyzzb3a ajv: 8.12.0 camelcase: 7.0.1 chokidar: 3.5.3 @@ -2503,23 +2524,23 @@ packages: - utf-8-validate dev: true - /@fluencelabs/cli/0.3.9_zefx7wxidm6hdha6gwewr46oqa: + /@fluencelabs/cli/0.3.9_qpxy5lgkhl6krmgytbtmcegdtu: resolution: {integrity: sha512-xJYi7+AHrWt6RgWnr7Efr8Jpv0dNLoWhiCMvgSbXoFpIZzQAyNDgk5hnCdwIQ/eiJcNg0GHY0gyC+Q/d5YKc1Q==} engines: {node: '=16'} hasBin: true dependencies: '@fluencelabs/aqua-api': 0.10.3 - '@fluencelabs/deal-aurora': 0.1.8_oxd5x4wivhg5zjyucvh2su6sbi - '@fluencelabs/fluence': 0.28.0_p34j5hgwlk5eyxias25f7ntdym + '@fluencelabs/deal-aurora': 0.1.8_gc7gatgk3jrcbnir3tz2izmvqi + '@fluencelabs/fluence': 0.28.0_zchmlqqvawdfhvpuz6nrqhtseq '@fluencelabs/fluence-network-environment': 1.0.14 '@iarna/toml': 2.2.5 '@mswjs/interceptors': 0.19.5 '@oclif/color': 1.0.4 - '@oclif/core': 2.6.2_j777nnsruz44drbtesvg2fqc7y - '@oclif/plugin-autocomplete': 2.1.4_j777nnsruz44drbtesvg2fqc7y - '@oclif/plugin-help': 5.2.7_j777nnsruz44drbtesvg2fqc7y - '@oclif/plugin-not-found': 2.3.21_j777nnsruz44drbtesvg2fqc7y - '@walletconnect/universal-provider': 2.4.7_me47trx7umfhug4hptdagpeso4 + '@oclif/core': 2.6.2_4bewfcp2iebiwuold25d6rgcsy + '@oclif/plugin-autocomplete': 2.1.4_4bewfcp2iebiwuold25d6rgcsy + '@oclif/plugin-help': 5.2.7_4bewfcp2iebiwuold25d6rgcsy + '@oclif/plugin-not-found': 2.3.21_4bewfcp2iebiwuold25d6rgcsy + '@walletconnect/universal-provider': 2.4.7_uz4bki5hahbpc2hwfvrjhsfeca ajv: 8.12.0 camelcase: 7.0.1 chokidar: 3.5.3 @@ -2624,10 +2645,10 @@ packages: - utf-8-validate dev: true - /@fluencelabs/deal-aurora/0.1.8_oxd5x4wivhg5zjyucvh2su6sbi: + /@fluencelabs/deal-aurora/0.1.8_gc7gatgk3jrcbnir3tz2izmvqi: resolution: {integrity: sha512-h2L3F67AsFxJy+mBAAUy8gMUGf85sgT3kuLhqEstdbQ20ASjxrSsXmyVZeVQLUx4nR1ygbGll9Y+FmRFgpNwMQ==} dependencies: - '@nomicfoundation/hardhat-toolbox': 1.0.2_oxd5x4wivhg5zjyucvh2su6sbi + '@nomicfoundation/hardhat-toolbox': 1.0.2_gc7gatgk3jrcbnir3tz2izmvqi '@openzeppelin/contracts': 4.8.2 '@openzeppelin/contracts-upgradeable': 4.8.2 dotenv: 16.0.3 @@ -2697,7 +2718,7 @@ packages: - utf-8-validate dev: true - /@fluencelabs/fluence/0.28.0_p34j5hgwlk5eyxias25f7ntdym: + /@fluencelabs/fluence/0.28.0_zchmlqqvawdfhvpuz6nrqhtseq: resolution: {integrity: sha512-SXb2vjTj8m/nw4jEILV0tu9VIFprGo8mNb2nOB5btxdsOI8GzQZkpGzTLrSd/+UagIo2GdxAu0GhBP8dxZXaqg==} engines: {node: '>=10', pnpm: '>=3'} hasBin: true @@ -2706,7 +2727,7 @@ packages: '@fluencelabs/connection': 0.2.0_node-fetch@2.6.9 '@fluencelabs/interfaces': 0.1.0 '@fluencelabs/keypair': 0.2.0 - '@fluencelabs/marine-js': 0.3.37_g4n3hsjlbmz4ag5o32ytojordu + '@fluencelabs/marine-js': 0.3.37_cnngzrja2umb46xxazlucyx2qu async: 3.2.4 base64-js: 1.5.1 browser-or-node: 2.0.0 @@ -2748,6 +2769,25 @@ packages: peer-id: 0.16.0 dev: true + /@fluencelabs/marine-js/0.3.37_cnngzrja2umb46xxazlucyx2qu: + resolution: {integrity: sha512-/Kpu3S+aDOfrOpKBAK1VeWSHKCoD36/dxtHEWHbj3Lsro0GB9zkoaZPHlFFL7rorCB+hyjAJqLDuBGI8f3l/qg==} + dependencies: + '@wasmer/wasi': 0.12.0 + '@wasmer/wasmfs': 0.12.0 + browser-or-node: 2.0.0 + buffer: 6.0.3 + threads: 1.7.0 + ts-jest: 27.1.5_cnngzrja2umb46xxazlucyx2qu + transitivePeerDependencies: + - '@babel/core' + - '@types/jest' + - babel-jest + - esbuild + - jest + - supports-color + - typescript + dev: true + /@fluencelabs/marine-js/0.3.37_g4n3hsjlbmz4ag5o32ytojordu: resolution: {integrity: sha512-/Kpu3S+aDOfrOpKBAK1VeWSHKCoD36/dxtHEWHbj3Lsro0GB9zkoaZPHlFFL7rorCB+hyjAJqLDuBGI8f3l/qg==} dependencies: @@ -3862,7 +3902,7 @@ packages: tweetnacl-util: 0.15.1 dev: true - /@morgan-stanley/ts-mocking-bird/0.6.4_g4n3hsjlbmz4ag5o32ytojordu: + /@morgan-stanley/ts-mocking-bird/0.6.4_cnngzrja2umb46xxazlucyx2qu: resolution: {integrity: sha512-57VJIflP8eR2xXa9cD1LUawh+Gh+BVQfVu0n6GALyg/AqV/Nz25kDRvws3i9kIe1PTrbsZZOYpsYp6bXPd6nVA==} peerDependencies: jasmine: 2.x || 3.x || 4.x @@ -3876,7 +3916,7 @@ packages: dependencies: jest: 27.5.1_ts-node@10.9.1 lodash: 4.17.21 - typescript: 4.7.4 + typescript: 4.9.5 uuid: 7.0.3 dev: true @@ -4131,7 +4171,7 @@ packages: chai-as-promised: 7.1.1_chai@4.3.7 deep-eql: 4.1.3 ethers: 5.7.2 - hardhat: 2.13.0_6oasmw356qmm23djlsjgkwvrtm + hardhat: 2.13.0_6qtx7vkbdhwvdm4crzlegk4mvi ordinal: 1.0.3 dev: true @@ -4141,10 +4181,10 @@ packages: hardhat: ^2.9.5 dependencies: ethereumjs-util: 7.1.5 - hardhat: 2.13.0_6oasmw356qmm23djlsjgkwvrtm + hardhat: 2.13.0_6qtx7vkbdhwvdm4crzlegk4mvi dev: true - /@nomicfoundation/hardhat-toolbox/1.0.2_oxd5x4wivhg5zjyucvh2su6sbi: + /@nomicfoundation/hardhat-toolbox/1.0.2_gc7gatgk3jrcbnir3tz2izmvqi: resolution: {integrity: sha512-8CEgWSKUK2aMit+76Sez8n7UB0Ze1lwT+LcWxj4EFP30lQWOwOws048t6MTPfThH0BlSWjC6hJRr0LncIkc1Sw==} peerDependencies: '@ethersproject/abi': ^5.4.7 @@ -4173,19 +4213,19 @@ packages: '@nomicfoundation/hardhat-network-helpers': 1.0.8_hardhat@2.13.0 '@nomiclabs/hardhat-ethers': 2.2.2_wknqauzjtp3mhprkntsmqpccee '@nomiclabs/hardhat-etherscan': 3.1.7_hardhat@2.13.0 - '@typechain/ethers-v5': 10.2.0_3j7r5nxxske4oddcicgvma5gkq + '@typechain/ethers-v5': 10.2.0_kuvqdvnhbslgxdpi2awxjzdvhe '@typechain/hardhat': 6.1.5_i2fwc4p7n2iltkzqmr3qbux6yq '@types/chai': 4.3.4 '@types/mocha': 9.1.1 '@types/node': 18.13.0 chai: 4.3.7 ethers: 5.7.2 - hardhat: 2.13.0_6oasmw356qmm23djlsjgkwvrtm + hardhat: 2.13.0_6qtx7vkbdhwvdm4crzlegk4mvi hardhat-gas-reporter: 1.0.9_hardhat@2.13.0 solidity-coverage: 0.7.22 - ts-node: 10.9.1_j777nnsruz44drbtesvg2fqc7y - typechain: 8.1.1_g4n3hsjlbmz4ag5o32ytojordu - typescript: 4.7.4 + ts-node: 10.9.1_4bewfcp2iebiwuold25d6rgcsy + typechain: 8.1.1_cnngzrja2umb46xxazlucyx2qu + typescript: 4.9.5 dev: true /@nomicfoundation/solidity-analyzer-darwin-arm64/0.1.1: @@ -4301,7 +4341,7 @@ packages: hardhat: ^2.0.0 dependencies: ethers: 5.7.2 - hardhat: 2.13.0_6oasmw356qmm23djlsjgkwvrtm + hardhat: 2.13.0_6qtx7vkbdhwvdm4crzlegk4mvi dev: true /@nomiclabs/hardhat-etherscan/3.1.7_hardhat@2.13.0: @@ -4315,7 +4355,7 @@ packages: chalk: 2.4.2 debug: 4.3.4 fs-extra: 7.0.1 - hardhat: 2.13.0_6oasmw356qmm23djlsjgkwvrtm + hardhat: 2.13.0_6qtx7vkbdhwvdm4crzlegk4mvi lodash: 4.17.21 semver: 6.3.0 table: 6.8.1 @@ -4335,7 +4375,7 @@ packages: tslib: 2.5.0 dev: true - /@oclif/core/2.6.2_j777nnsruz44drbtesvg2fqc7y: + /@oclif/core/2.6.2_4bewfcp2iebiwuold25d6rgcsy: resolution: {integrity: sha512-roxcBLr4BuoOEDEkMQk4Yy0Tolr39n6i+A63qPLa19vrgxjZZJygh2HpThsn69/UPuEzMq051FnvJ9tNln3Y5g==} engines: {node: '>=14.0.0'} dependencies: @@ -4363,7 +4403,7 @@ packages: strip-ansi: 6.0.1 supports-color: 8.1.1 supports-hyperlinks: 2.3.0 - ts-node: 10.9.1_j777nnsruz44drbtesvg2fqc7y + ts-node: 10.9.1_4bewfcp2iebiwuold25d6rgcsy tslib: 2.5.0 widest-line: 3.1.0 wordwrap: 1.0.0 @@ -4375,11 +4415,11 @@ packages: - typescript dev: true - /@oclif/plugin-autocomplete/2.1.4_j777nnsruz44drbtesvg2fqc7y: + /@oclif/plugin-autocomplete/2.1.4_4bewfcp2iebiwuold25d6rgcsy: resolution: {integrity: sha512-MwjXXE05fho7bMuilCbXt7DAc5jFKnKMzeE95VUOCCc390Ma9fmY32sNZH/COkJxnxBhFj7rUVy/PiCqimBiyQ==} engines: {node: '>=12.0.0'} dependencies: - '@oclif/core': 2.6.2_j777nnsruz44drbtesvg2fqc7y + '@oclif/core': 2.6.2_4bewfcp2iebiwuold25d6rgcsy chalk: 4.1.2 debug: 4.3.4 fs-extra: 9.1.0 @@ -4391,11 +4431,11 @@ packages: - typescript dev: true - /@oclif/plugin-help/5.2.7_j777nnsruz44drbtesvg2fqc7y: + /@oclif/plugin-help/5.2.7_4bewfcp2iebiwuold25d6rgcsy: resolution: {integrity: sha512-p6DJk0QMfzGvXGqrSZTQPitUGi68oGzr48tRKdrVxDDPMAELxHeXXFbkNRNdvjldPpTk44m0PbxV5tWhwurRwg==} engines: {node: '>=12.0.0'} dependencies: - '@oclif/core': 2.6.2_j777nnsruz44drbtesvg2fqc7y + '@oclif/core': 2.6.2_4bewfcp2iebiwuold25d6rgcsy transitivePeerDependencies: - '@swc/core' - '@swc/wasm' @@ -4403,12 +4443,12 @@ packages: - typescript dev: true - /@oclif/plugin-not-found/2.3.21_j777nnsruz44drbtesvg2fqc7y: + /@oclif/plugin-not-found/2.3.21_4bewfcp2iebiwuold25d6rgcsy: resolution: {integrity: sha512-6N0gTWQhl5nuE5bXipv5+8vH5cHyGgEd3MKJHQYxFnHIFwwb9jJnFl0BZ0fo7Jrjd9HZYCLT7rjnouS7p1Dl1w==} engines: {node: '>=12.0.0'} dependencies: '@oclif/color': 1.0.4 - '@oclif/core': 2.6.2_j777nnsruz44drbtesvg2fqc7y + '@oclif/core': 2.6.2_4bewfcp2iebiwuold25d6rgcsy fast-levenshtein: 3.0.0 lodash: 4.17.21 transitivePeerDependencies: @@ -5144,7 +5184,7 @@ packages: resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==} dev: true - /@typechain/ethers-v5/10.2.0_3j7r5nxxske4oddcicgvma5gkq: + /@typechain/ethers-v5/10.2.0_kuvqdvnhbslgxdpi2awxjzdvhe: resolution: {integrity: sha512-ikaq0N/w9fABM+G01OFmU3U3dNnyRwEahkdvi9mqy1a3XwKiPZaF/lu54OcNaEWnpvEYyhhS0N7buCtLQqC92w==} peerDependencies: '@ethersproject/abi': ^5.0.0 @@ -5159,9 +5199,9 @@ packages: '@ethersproject/providers': 5.7.2 ethers: 5.7.2 lodash: 4.17.21 - ts-essentials: 7.0.3_typescript@4.7.4 - typechain: 8.1.1_g4n3hsjlbmz4ag5o32ytojordu - typescript: 4.7.4 + ts-essentials: 7.0.3_typescript@4.9.5 + typechain: 8.1.1_cnngzrja2umb46xxazlucyx2qu + typescript: 4.9.5 dev: true /@typechain/hardhat/6.1.5_i2fwc4p7n2iltkzqmr3qbux6yq: @@ -5176,11 +5216,11 @@ packages: dependencies: '@ethersproject/abi': 5.7.0 '@ethersproject/providers': 5.7.2 - '@typechain/ethers-v5': 10.2.0_3j7r5nxxske4oddcicgvma5gkq + '@typechain/ethers-v5': 10.2.0_kuvqdvnhbslgxdpi2awxjzdvhe ethers: 5.7.2 fs-extra: 9.1.0 - hardhat: 2.13.0_6oasmw356qmm23djlsjgkwvrtm - typechain: 8.1.1_g4n3hsjlbmz4ag5o32ytojordu + hardhat: 2.13.0_6qtx7vkbdhwvdm4crzlegk4mvi + typechain: 8.1.1_cnngzrja2umb46xxazlucyx2qu dev: true /@types/aria-query/5.0.1: @@ -5795,10 +5835,10 @@ packages: pretty-format: 27.5.1 dev: true - /@walletconnect/core/2.4.7_me47trx7umfhug4hptdagpeso4: + /@walletconnect/core/2.4.7_mqijgjbnahaa3tnbkktxyzzb3a: resolution: {integrity: sha512-w92NrtziqrWs070HJICGh80Vp60PaXu06OjNvOnVZEorbTipCWx4xxgcC2NhsT4TCQ8r1FOut6ahLe1PILuRsg==} dependencies: - '@walletconnect/heartbeat': 1.2.0_j777nnsruz44drbtesvg2fqc7y + '@walletconnect/heartbeat': 1.2.0_4bewfcp2iebiwuold25d6rgcsy '@walletconnect/jsonrpc-provider': 1.0.8 '@walletconnect/jsonrpc-utils': 1.0.6 '@walletconnect/jsonrpc-ws-connection': 1.0.9 @@ -5808,8 +5848,8 @@ packages: '@walletconnect/relay-auth': 1.0.4 '@walletconnect/safe-json': 1.0.1 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.4.7_me47trx7umfhug4hptdagpeso4 - '@walletconnect/utils': 2.4.7_me47trx7umfhug4hptdagpeso4 + '@walletconnect/types': 2.4.7_mqijgjbnahaa3tnbkktxyzzb3a + '@walletconnect/utils': 2.4.7_mqijgjbnahaa3tnbkktxyzzb3a events: 3.3.0 lodash.isequal: 4.5.0 pino: 7.11.0 @@ -5838,14 +5878,14 @@ packages: tslib: 1.14.1 dev: true - /@walletconnect/heartbeat/1.2.0_j777nnsruz44drbtesvg2fqc7y: + /@walletconnect/heartbeat/1.2.0_4bewfcp2iebiwuold25d6rgcsy: resolution: {integrity: sha512-0vbzTa/ARrpmMmOD+bQMxPvFYKtOLQZObgZakrYr0aODiMOO71CmPVNV2eAqXnw9rMmcP+z91OybLeIFlwTjjA==} dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/time': 1.0.2 chai: 4.3.7 mocha: 10.2.0 - ts-node: 10.9.1_j777nnsruz44drbtesvg2fqc7y + ts-node: 10.9.1_4bewfcp2iebiwuold25d6rgcsy tslib: 1.14.1 transitivePeerDependencies: - '@swc/core' @@ -5948,18 +5988,18 @@ packages: tslib: 1.14.1 dev: true - /@walletconnect/sign-client/2.4.7_me47trx7umfhug4hptdagpeso4: + /@walletconnect/sign-client/2.4.7_mqijgjbnahaa3tnbkktxyzzb3a: resolution: {integrity: sha512-x5uxnHQkNSn0QNXUdPEfwy4o1Vyi2QIWkDGUh+pfSP4s2vN0+IJAcwqBqkPn+zJ1X7eKYLs+v0ih1eieciYMPA==} dependencies: - '@walletconnect/core': 2.4.7_me47trx7umfhug4hptdagpeso4 + '@walletconnect/core': 2.4.7_mqijgjbnahaa3tnbkktxyzzb3a '@walletconnect/events': 1.0.1 - '@walletconnect/heartbeat': 1.2.0_j777nnsruz44drbtesvg2fqc7y + '@walletconnect/heartbeat': 1.2.0_4bewfcp2iebiwuold25d6rgcsy '@walletconnect/jsonrpc-provider': 1.0.8 '@walletconnect/jsonrpc-utils': 1.0.6 '@walletconnect/logger': 2.0.1 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.4.7_me47trx7umfhug4hptdagpeso4 - '@walletconnect/utils': 2.4.7_me47trx7umfhug4hptdagpeso4 + '@walletconnect/types': 2.4.7_mqijgjbnahaa3tnbkktxyzzb3a + '@walletconnect/utils': 2.4.7_mqijgjbnahaa3tnbkktxyzzb3a events: 3.3.0 pino: 7.11.0 transitivePeerDependencies: @@ -5979,11 +6019,11 @@ packages: tslib: 1.14.1 dev: true - /@walletconnect/types/2.4.7_me47trx7umfhug4hptdagpeso4: + /@walletconnect/types/2.4.7_mqijgjbnahaa3tnbkktxyzzb3a: resolution: {integrity: sha512-1VaPdPJrE+UrEjAhK5bdxq2+MTo3DvUMmQeNUsp3vUGhocQXB9hJQQ1rYBknYYSyDu2rTksGCQ4nv3ZOqfxvHw==} dependencies: '@walletconnect/events': 1.0.1 - '@walletconnect/heartbeat': 1.2.0_j777nnsruz44drbtesvg2fqc7y + '@walletconnect/heartbeat': 1.2.0_4bewfcp2iebiwuold25d6rgcsy '@walletconnect/jsonrpc-types': 1.0.2 '@walletconnect/keyvaluestorage': 1.0.2_lokijs@1.5.12 '@walletconnect/logger': 2.0.1 @@ -5997,7 +6037,7 @@ packages: - typescript dev: true - /@walletconnect/universal-provider/2.4.7_6ceuysb5murbu27s24v75mmoaq: + /@walletconnect/universal-provider/2.4.7_mqijgjbnahaa3tnbkktxyzzb3a: resolution: {integrity: sha512-xlefq2ahAsH3SpcsofWQQ5JT3Tz9NLAViA8FW07PHhfuf9p7OLp+Mu1wKxQEoBilyvfYRF4R5MTyTPy1wqJiRA==} dependencies: '@walletconnect/jsonrpc-http-connection': 1.0.6 @@ -6005,36 +6045,9 @@ packages: '@walletconnect/jsonrpc-types': 1.0.2 '@walletconnect/jsonrpc-utils': 1.0.6 '@walletconnect/logger': 2.0.1 - '@walletconnect/sign-client': 2.4.7_me47trx7umfhug4hptdagpeso4 - '@walletconnect/types': 2.4.7_me47trx7umfhug4hptdagpeso4 - '@walletconnect/utils': 2.4.7_me47trx7umfhug4hptdagpeso4 - eip1193-provider: 1.0.1_debug@4.3.4 - events: 3.3.0 - pino: 7.11.0 - transitivePeerDependencies: - - '@react-native-async-storage/async-storage' - - '@swc/core' - - '@swc/wasm' - - '@types/node' - - bufferutil - - debug - - encoding - - lokijs - - typescript - - utf-8-validate - dev: true - - /@walletconnect/universal-provider/2.4.7_me47trx7umfhug4hptdagpeso4: - resolution: {integrity: sha512-xlefq2ahAsH3SpcsofWQQ5JT3Tz9NLAViA8FW07PHhfuf9p7OLp+Mu1wKxQEoBilyvfYRF4R5MTyTPy1wqJiRA==} - dependencies: - '@walletconnect/jsonrpc-http-connection': 1.0.6 - '@walletconnect/jsonrpc-provider': 1.0.8 - '@walletconnect/jsonrpc-types': 1.0.2 - '@walletconnect/jsonrpc-utils': 1.0.6 - '@walletconnect/logger': 2.0.1 - '@walletconnect/sign-client': 2.4.7_me47trx7umfhug4hptdagpeso4 - '@walletconnect/types': 2.4.7_me47trx7umfhug4hptdagpeso4 - '@walletconnect/utils': 2.4.7_me47trx7umfhug4hptdagpeso4 + '@walletconnect/sign-client': 2.4.7_mqijgjbnahaa3tnbkktxyzzb3a + '@walletconnect/types': 2.4.7_mqijgjbnahaa3tnbkktxyzzb3a + '@walletconnect/utils': 2.4.7_mqijgjbnahaa3tnbkktxyzzb3a eip1193-provider: 1.0.1 events: 3.3.0 pino: 7.11.0 @@ -6051,7 +6064,34 @@ packages: - utf-8-validate dev: true - /@walletconnect/utils/2.4.7_me47trx7umfhug4hptdagpeso4: + /@walletconnect/universal-provider/2.4.7_uz4bki5hahbpc2hwfvrjhsfeca: + resolution: {integrity: sha512-xlefq2ahAsH3SpcsofWQQ5JT3Tz9NLAViA8FW07PHhfuf9p7OLp+Mu1wKxQEoBilyvfYRF4R5MTyTPy1wqJiRA==} + dependencies: + '@walletconnect/jsonrpc-http-connection': 1.0.6 + '@walletconnect/jsonrpc-provider': 1.0.8 + '@walletconnect/jsonrpc-types': 1.0.2 + '@walletconnect/jsonrpc-utils': 1.0.6 + '@walletconnect/logger': 2.0.1 + '@walletconnect/sign-client': 2.4.7_mqijgjbnahaa3tnbkktxyzzb3a + '@walletconnect/types': 2.4.7_mqijgjbnahaa3tnbkktxyzzb3a + '@walletconnect/utils': 2.4.7_mqijgjbnahaa3tnbkktxyzzb3a + eip1193-provider: 1.0.1_debug@4.3.4 + events: 3.3.0 + pino: 7.11.0 + transitivePeerDependencies: + - '@react-native-async-storage/async-storage' + - '@swc/core' + - '@swc/wasm' + - '@types/node' + - bufferutil + - debug + - encoding + - lokijs + - typescript + - utf-8-validate + dev: true + + /@walletconnect/utils/2.4.7_mqijgjbnahaa3tnbkktxyzzb3a: resolution: {integrity: sha512-t3kW0qLClnejTTKg3y/o/MmJb5ZDGfD13YT9Nw56Up3qq/pwVfTtWjt8vJOQWMIm0hZgjgESivcf6/wuu3/Oqw==} dependencies: '@stablelib/chacha20poly1305': 1.0.1 @@ -6063,7 +6103,7 @@ packages: '@walletconnect/relay-api': 1.0.9 '@walletconnect/safe-json': 1.0.1 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.4.7_me47trx7umfhug4hptdagpeso4 + '@walletconnect/types': 2.4.7_mqijgjbnahaa3tnbkktxyzzb3a '@walletconnect/window-getters': 1.0.1 '@walletconnect/window-metadata': 1.0.1 detect-browser: 5.3.0 @@ -10874,13 +10914,13 @@ packages: dependencies: array-uniq: 1.0.3 eth-gas-reporter: 0.2.25 - hardhat: 2.13.0_6oasmw356qmm23djlsjgkwvrtm + hardhat: 2.13.0_6qtx7vkbdhwvdm4crzlegk4mvi sha1: 1.1.1 transitivePeerDependencies: - '@codechecks/client' dev: true - /hardhat/2.13.0_6oasmw356qmm23djlsjgkwvrtm: + /hardhat/2.13.0_6qtx7vkbdhwvdm4crzlegk4mvi: resolution: {integrity: sha512-ZlzBOLML1QGlm6JWyVAG8lVTEAoOaVm1in/RU2zoGAnYEoD1Rp4T+ZMvrLNhHaaeS9hfjJ1gJUBfiDr4cx+htQ==} engines: {node: '>=14.0.0'} hasBin: true @@ -10939,9 +10979,9 @@ packages: solc: 0.7.3_debug@4.3.4 source-map-support: 0.5.21 stacktrace-parser: 0.1.10 - ts-node: 10.9.1_j777nnsruz44drbtesvg2fqc7y + ts-node: 10.9.1_4bewfcp2iebiwuold25d6rgcsy tsort: 0.0.1 - typescript: 4.7.4 + typescript: 4.9.5 undici: 5.18.0 uuid: 8.3.2 ws: 7.5.9 @@ -12646,7 +12686,7 @@ packages: pretty-format: 27.5.1 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1_j777nnsruz44drbtesvg2fqc7y + ts-node: 10.9.1_4bewfcp2iebiwuold25d6rgcsy transitivePeerDependencies: - bufferutil - canvas @@ -18208,7 +18248,7 @@ packages: dependencies: command-exists: 1.2.9 commander: 3.0.2 - follow-redirects: 1.15.2_debug@4.3.4 + follow-redirects: 1.15.2 fs-extra: 0.30.0 js-sha3: 0.8.0 memorystream: 0.3.1 @@ -19151,11 +19191,11 @@ packages: resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==} dev: false - /ts-command-line-args/2.4.2_g4n3hsjlbmz4ag5o32ytojordu: + /ts-command-line-args/2.4.2_cnngzrja2umb46xxazlucyx2qu: resolution: {integrity: sha512-mJLQQBOdyD4XI/ZWQY44PIdYde47JhV2xl380O7twPkTQ+Y5vFDHsk8LOeXKuz7dVY5aDCfAzRarNfSqtKOkQQ==} hasBin: true dependencies: - '@morgan-stanley/ts-mocking-bird': 0.6.4_g4n3hsjlbmz4ag5o32ytojordu + '@morgan-stanley/ts-mocking-bird': 0.6.4_cnngzrja2umb46xxazlucyx2qu chalk: 4.1.2 command-line-args: 5.2.1 command-line-usage: 6.1.3 @@ -19166,12 +19206,45 @@ packages: - typescript dev: true - /ts-essentials/7.0.3_typescript@4.7.4: + /ts-essentials/7.0.3_typescript@4.9.5: resolution: {integrity: sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==} peerDependencies: typescript: '>=3.7.0' dependencies: - typescript: 4.7.4 + typescript: 4.9.5 + dev: true + + /ts-jest/27.1.5_cnngzrja2umb46xxazlucyx2qu: + resolution: {integrity: sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@types/jest': ^27.0.0 + babel-jest: '>=27.0.0 <28' + esbuild: '*' + jest: ^27.0.0 + typescript: '>=3.8 <5.0' + peerDependenciesMeta: + '@babel/core': + optional: true + '@types/jest': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 27.5.1_ts-node@10.9.1 + jest-util: 27.5.1 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.3.8 + typescript: 4.9.5 + yargs-parser: 20.2.9 dev: true /ts-jest/27.1.5_g4n3hsjlbmz4ag5o32ytojordu: @@ -19207,7 +19280,7 @@ packages: yargs-parser: 20.2.9 dev: true - /ts-jest/28.0.2_byf75w6xilfwy3ncjzlldwxox4: + /ts-jest/28.0.2_m4pn7vsromlf5ffrouypoapnnq: resolution: {integrity: sha512-IOZMb3D0gx6IHO9ywPgiQxJ3Zl4ECylEFwoVpENB55aTn5sdO0Ptyx/7noNBxAaUff708RqQL4XBNxxOVjY0vQ==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} hasBin: true @@ -19238,10 +19311,41 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.3.8 - typescript: 4.7.4 + typescript: 4.9.5 yargs-parser: 20.2.9 dev: true + /ts-node/10.9.1_4bewfcp2iebiwuold25d6rgcsy: + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.3 + '@types/node': 18.13.0 + acorn: 8.8.2 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + /ts-node/10.9.1_j777nnsruz44drbtesvg2fqc7y: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true @@ -19276,7 +19380,7 @@ packages: /ts-pattern/3.3.3: resolution: {integrity: sha512-Z5EFi6g6wyX3uDFHqxF5W5c5h663oZg9O6aOiAT7fqNu0HPSfCxtHzrQ7SblTy738Mrg2Ezorky8H5aUOm8Pvg==} - /tsconfck/2.0.2_typescript@4.7.4: + /tsconfck/2.0.2_typescript@4.9.5: resolution: {integrity: sha512-H3DWlwKpow+GpVLm/2cpmok72pwRr1YFROV3YzAmvzfGFiC1zEM/mc9b7+1XnrxuXtEbhJ7xUSIqjPFbedp7aQ==} engines: {node: ^14.13.1 || ^16 || >=18, pnpm: ^7.18.0} hasBin: true @@ -19286,7 +19390,7 @@ packages: typescript: optional: true dependencies: - typescript: 4.7.4 + typescript: 4.9.5 dev: true /tsconfig-paths/3.14.1: @@ -19392,7 +19496,7 @@ packages: resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==} dev: true - /typechain/8.1.1_g4n3hsjlbmz4ag5o32ytojordu: + /typechain/8.1.1_cnngzrja2umb46xxazlucyx2qu: resolution: {integrity: sha512-uF/sUvnXTOVF2FHKhQYnxHk4su4JjZR8vr4mA2mBaRwHTbwh0jIlqARz9XJr1tA0l7afJGvEa1dTSi4zt039LQ==} hasBin: true peerDependencies: @@ -19406,9 +19510,9 @@ packages: lodash: 4.17.21 mkdirp: 1.0.4 prettier: 2.8.4 - ts-command-line-args: 2.4.2_g4n3hsjlbmz4ag5o32ytojordu - ts-essentials: 7.0.3_typescript@4.7.4 - typescript: 4.7.4 + ts-command-line-args: 2.4.2_cnngzrja2umb46xxazlucyx2qu + ts-essentials: 7.0.3_typescript@4.9.5 + typescript: 4.9.5 transitivePeerDependencies: - jasmine - jest @@ -19777,14 +19881,14 @@ packages: vite: 4.0.4_@types+node@16.11.59 dev: true - /vite-tsconfig-paths/4.0.3_trrwuuiz4f5khno7hdf3cjz2ky: + /vite-tsconfig-paths/4.0.3_egung5nfepmolqa7uavvqho3gq: resolution: {integrity: sha512-gRO2Q/tOkV+9kMht5tz90+IaEKvW2zCnvwJV3tp2ruPNZOTM5rF+yXorJT4ggmAMYEaJ3nyXjx5P5jY5FwiZ+A==} peerDependencies: vite: '>2.0.0-0' dependencies: debug: 4.3.4 globrex: 0.1.2 - tsconfck: 2.0.2_typescript@4.7.4 + tsconfck: 2.0.2_typescript@4.9.5 vite: 4.0.4_@types+node@16.11.59 transitivePeerDependencies: - supports-color