diff --git a/packages/fluence-js/aqua/src/node-utils.aqua b/packages/fluence-js/aqua/src/node-utils.aqua new file mode 100644 index 000000000..71986100c --- /dev/null +++ b/packages/fluence-js/aqua/src/node-utils.aqua @@ -0,0 +1,12 @@ +data ReadFileResult: + -- Was the call successful or not + success: bool + -- File content in base64 if the call was successful + content: ?string + -- Error message if the call was unsuccessful + error: ?string + +service NodeUtils("node_utils"): + -- Read file from file system. + -- returns file content in base64 format + read_file(path: string) -> ReadFileResult diff --git a/packages/fluence-js/aqua/src/single-module-srv.aqua b/packages/fluence-js/aqua/src/single-module-srv.aqua new file mode 100644 index 000000000..78547d764 --- /dev/null +++ b/packages/fluence-js/aqua/src/single-module-srv.aqua @@ -0,0 +1,32 @@ +alias Bytes : []u8 + +data ServiceCreationResult: + success: bool + service_id: ?string + error: ?string + +data ReadFileResult: + success: bool + content: ?string + error: ?string + +data RemoveResult: + success: bool + error: ?string + +alias ListServiceResult: []string + +service Srv("single_module_srv"): + -- Used to create a service on a certain node + -- Arguments: + -- bytes – a base64 string containing the .wasm module to add. + -- Returns: service_id – the service ID of the created service. + create(wasm_b64_content: string) -> ServiceCreationResult + + -- Used to remove a service from a certain node + -- Arguments: + -- service_id – ID of the service to remove + remove(service_id: string) -> RemoveResult + + -- Returns a list of services ids running on a peer + list() -> ListServiceResult diff --git a/packages/fluence-js/aqua/tests/srv-tests.aqua b/packages/fluence-js/aqua/tests/srv-tests.aqua new file mode 100644 index 000000000..f390062ef --- /dev/null +++ b/packages/fluence-js/aqua/tests/srv-tests.aqua @@ -0,0 +1,40 @@ +module Export + +import Srv from "../src/single-module-srv.aqua" +import NodeUtils from "../src/node-utils.aqua" +export happy_path, list_services, file_not_found, service_removed, removing_non_exiting + +service Greeting("greeting"): + greeting(name: string) -> string + +func happy_path(file_path: string) -> string: + file <- NodeUtils.read_file(file_path) + created_service <- Srv.create(file.content!) + Greeting created_service.service_id! + <- Greeting.greeting("test") + +func list_services(file_path: string) -> []string: + file <- NodeUtils.read_file(file_path) + Srv.create(file.content!) + Srv.create(file.content!) + Srv.create(file.content!) + <- Srv.list() + +func file_not_found() -> string: + e <- NodeUtils.read_file("/random/incorrect/file") + <- e.error! + +func service_removed(file_path: string) -> string: + file <- NodeUtils.read_file(file_path) + created_service <- Srv.create(file.content!) + Greeting created_service.service_id! + Srv.remove(created_service.service_id!) + try: + dontcare <- Greeting.greeting("test") + catch e: + <- e.message + +func removing_non_exiting() -> string: + e <- Srv.remove("random_id") + <- e.error! + diff --git a/packages/fluence-js/src/__test__/_aqua/marine-js-logging.ts b/packages/fluence-js/src/__test__/_aqua/marine-js-logging.ts index 9d4a1a43c..96690aa2b 100644 --- a/packages/fluence-js/src/__test__/_aqua/marine-js-logging.ts +++ b/packages/fluence-js/src/__test__/_aqua/marine-js-logging.ts @@ -3,30 +3,31 @@ * This file is auto-generated. Do not edit manually: changes may be erased. * Generated by Aqua compiler: https://github.com/fluencelabs/aqua/. * If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues - * Aqua version: 0.7.2-314 + * Aqua version: 0.7.7-362 * */ -import { Fluence, FluencePeer } from '../../index'; -import { CallParams, callFunction, registerService } from '../../internal/compilerSupport/v3'; +import { FluencePeer } from '../../index'; +import type { CallParams$$ } from '../../internal/compilerSupport/v4'; +import { callFunction$$, registerService$$ } from '../../internal/compilerSupport/v4'; // Services export interface GreetingRecordDef { greeting_record: ( - callParams: CallParams, + callParams: CallParams$$, ) => { num: number; str: string } | Promise<{ num: number; str: string }>; - log_debug: (callParams: CallParams) => void | Promise; - log_error: (callParams: CallParams) => void | Promise; - log_info: (callParams: CallParams) => void | Promise; - log_trace: (callParams: CallParams) => void | Promise; - log_warn: (callParams: CallParams) => void | Promise; - void_fn: (callParams: CallParams) => void | Promise; + log_debug: (callParams: CallParams$$) => void | Promise; + log_error: (callParams: CallParams$$) => void | Promise; + log_info: (callParams: CallParams$$) => void | Promise; + log_trace: (callParams: CallParams$$) => void | Promise; + log_warn: (callParams: CallParams$$) => void | Promise; + void_fn: (callParams: CallParams$$) => void | Promise; } export function registerGreetingRecord(serviceId: string, service: GreetingRecordDef): void; export function registerGreetingRecord(peer: FluencePeer, serviceId: string, service: GreetingRecordDef): void; export function registerGreetingRecord(...args: any) { - registerService(args, { + registerService$$(args, { functions: { tag: 'labeledProduct', fields: { @@ -133,7 +134,7 @@ export function call_info(...args: any) { (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 1]) ) `; - return callFunction( + return callFunction$$( args, { functionName: 'call_info', diff --git a/packages/fluence-js/src/__test__/_aqua/marine-js.ts b/packages/fluence-js/src/__test__/_aqua/marine-js.ts index a92fb0e1a..19afb8c2b 100644 --- a/packages/fluence-js/src/__test__/_aqua/marine-js.ts +++ b/packages/fluence-js/src/__test__/_aqua/marine-js.ts @@ -3,18 +3,19 @@ * This file is auto-generated. Do not edit manually: changes may be erased. * Generated by Aqua compiler: https://github.com/fluencelabs/aqua/. * If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues - * Aqua version: 0.7.2-314 + * Aqua version: 0.7.7-362 * */ -import { Fluence, FluencePeer } from '../../index'; -import { CallParams, callFunction, registerService } from '../../internal/compilerSupport/v3'; +import { FluencePeer } from '../../index'; +import type { CallParams$$ } from '../../internal/compilerSupport/v4'; +import { callFunction$$, registerService$$ } from '../../internal/compilerSupport/v4'; // Services export interface GreetingDef { - greeting: (name: string, callParams: CallParams<'name'>) => string | Promise; + greeting: (name: string, callParams: CallParams$$<'name'>) => string | Promise; greeting_record: ( - callParams: CallParams, + callParams: CallParams$$, ) => { num: number; str: string } | Promise<{ num: number; str: string }>; } export function registerGreeting(service: GreetingDef): void; @@ -23,7 +24,7 @@ export function registerGreeting(peer: FluencePeer, service: GreetingDef): void; export function registerGreeting(peer: FluencePeer, serviceId: string, service: GreetingDef): void; export function registerGreeting(...args: any) { - registerService(args, { + registerService$$(args, { defaultServiceId: 'greeting', functions: { tag: 'labeledProduct', @@ -110,7 +111,7 @@ export function call(...args: any) { (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2]) ) `; - return callFunction( + return callFunction$$( args, { functionName: 'call', diff --git a/packages/fluence-js/src/__test__/_aqua/sig-tests.ts b/packages/fluence-js/src/__test__/_aqua/sig-tests.ts index b03cfa3c1..4beeb34a2 100644 --- a/packages/fluence-js/src/__test__/_aqua/sig-tests.ts +++ b/packages/fluence-js/src/__test__/_aqua/sig-tests.ts @@ -3,16 +3,17 @@ * This file is auto-generated. Do not edit manually: changes may be erased. * Generated by Aqua compiler: https://github.com/fluencelabs/aqua/. * If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues - * Aqua version: 0.7.2-314 + * Aqua version: 0.7.7-362 * */ -import { Fluence, FluencePeer } from '../../index'; -import { CallParams, callFunction, registerService } from '../../internal/compilerSupport/v3'; +import { FluencePeer } from '../../index'; +import type { CallParams$$ } from '../../internal/compilerSupport/v4'; +import { callFunction$$, registerService$$ } from '../../internal/compilerSupport/v4'; // Services export interface DataProviderDef { - provide_data: (callParams: CallParams) => number[] | Promise; + provide_data: (callParams: CallParams$$) => number[] | Promise; } export function registerDataProvider(service: DataProviderDef): void; export function registerDataProvider(serviceId: string, service: DataProviderDef): void; @@ -20,7 +21,7 @@ export function registerDataProvider(peer: FluencePeer, service: DataProviderDef export function registerDataProvider(peer: FluencePeer, serviceId: string, service: DataProviderDef): void; export function registerDataProvider(...args: any) { - registerService(args, { + registerService$$(args, { defaultServiceId: 'data', functions: { tag: 'labeledProduct', @@ -49,17 +50,17 @@ export function registerDataProvider(...args: any) { } export interface SigDef { - get_peer_id: (callParams: CallParams) => string | Promise; + get_peer_id: (callParams: CallParams$$) => string | Promise; sign: ( data: number[], - callParams: CallParams<'data'>, + callParams: CallParams$$<'data'>, ) => | { error: string | null; signature: number[] | null; success: boolean } | Promise<{ error: string | null; signature: number[] | null; success: boolean }>; verify: ( signature: number[], data: number[], - callParams: CallParams<'signature' | 'data'>, + callParams: CallParams$$<'signature' | 'data'>, ) => boolean | Promise; } export function registerSig(service: SigDef): void; @@ -68,7 +69,7 @@ export function registerSig(peer: FluencePeer, service: SigDef): void; export function registerSig(peer: FluencePeer, serviceId: string, service: SigDef): void; export function registerSig(...args: any) { - registerService(args, { + registerService$$(args, { defaultServiceId: 'sig', functions: { tag: 'labeledProduct', @@ -200,7 +201,7 @@ export function callSig(...args: any) { (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2]) ) `; - return callFunction( + return callFunction$$( args, { functionName: 'callSig', diff --git a/packages/fluence-js/src/__test__/_aqua/srv-tests.ts b/packages/fluence-js/src/__test__/_aqua/srv-tests.ts new file mode 100644 index 000000000..e301b25ce --- /dev/null +++ b/packages/fluence-js/src/__test__/_aqua/srv-tests.ts @@ -0,0 +1,341 @@ +/** + * + * This file is auto-generated. Do not edit manually: changes may be erased. + * Generated by Aqua compiler: https://github.com/fluencelabs/aqua/. + * If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues + * Aqua version: 0.7.7-362 + * + */ +import { FluencePeer } from '../../index'; +import { callFunction$$ } from '../../internal/compilerSupport/v4'; + +// Services + +// Functions + +export function happy_path(file_path: string, config?: { ttl?: number }): Promise; + +export function happy_path(peer: FluencePeer, file_path: string, config?: { ttl?: number }): Promise; + +export function happy_path(...args: any) { + let script = ` + (xor + (seq + (seq + (seq + (seq + (seq + (call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-) + (call %init_peer_id% ("getDataSrv" "file_path") [] file_path) + ) + (call %init_peer_id% ("node_utils" "read_file") [file_path] file) + ) + (call %init_peer_id% ("single_module_srv" "create") [file.$.content.[0]!] created_service) + ) + (call %init_peer_id% (created_service.$.service_id.[0]! "greeting") ["test"] greeting) + ) + (xor + (call %init_peer_id% ("callbackSrv" "response") [greeting]) + (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 1]) + ) + ) + (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2]) + ) + `; + return callFunction$$( + args, + { + functionName: 'happy_path', + arrow: { + tag: 'arrow', + domain: { + tag: 'labeledProduct', + fields: { + file_path: { + tag: 'scalar', + name: 'string', + }, + }, + }, + codomain: { + tag: 'unlabeledProduct', + items: [ + { + tag: 'scalar', + name: 'string', + }, + ], + }, + }, + names: { + relay: '-relay-', + getDataSrv: 'getDataSrv', + callbackSrv: 'callbackSrv', + responseSrv: 'callbackSrv', + responseFnName: 'response', + errorHandlingSrv: 'errorHandlingSrv', + errorFnName: 'error', + }, + }, + script, + ); +} + +export function removing_non_exiting(config?: { ttl?: number }): Promise; + +export function removing_non_exiting(peer: FluencePeer, config?: { ttl?: number }): Promise; + +export function removing_non_exiting(...args: any) { + let script = ` + (xor + (seq + (seq + (call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-) + (call %init_peer_id% ("single_module_srv" "remove") ["random_id"] e) + ) + (xor + (call %init_peer_id% ("callbackSrv" "response") [e.$.error.[0]!]) + (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 1]) + ) + ) + (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2]) + ) + `; + return callFunction$$( + args, + { + functionName: 'removing_non_exiting', + arrow: { + tag: 'arrow', + domain: { + tag: 'labeledProduct', + fields: {}, + }, + codomain: { + tag: 'unlabeledProduct', + items: [ + { + tag: 'scalar', + name: 'string', + }, + ], + }, + }, + names: { + relay: '-relay-', + getDataSrv: 'getDataSrv', + callbackSrv: 'callbackSrv', + responseSrv: 'callbackSrv', + responseFnName: 'response', + errorHandlingSrv: 'errorHandlingSrv', + errorFnName: 'error', + }, + }, + script, + ); +} + +export function file_not_found(config?: { ttl?: number }): Promise; + +export function file_not_found(peer: FluencePeer, config?: { ttl?: number }): Promise; + +export function file_not_found(...args: any) { + let script = ` + (xor + (seq + (seq + (call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-) + (call %init_peer_id% ("node_utils" "read_file") ["/random/incorrect/file"] e) + ) + (xor + (call %init_peer_id% ("callbackSrv" "response") [e.$.error.[0]!]) + (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 1]) + ) + ) + (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2]) + ) + `; + return callFunction$$( + args, + { + functionName: 'file_not_found', + arrow: { + tag: 'arrow', + domain: { + tag: 'labeledProduct', + fields: {}, + }, + codomain: { + tag: 'unlabeledProduct', + items: [ + { + tag: 'scalar', + name: 'string', + }, + ], + }, + }, + names: { + relay: '-relay-', + getDataSrv: 'getDataSrv', + callbackSrv: 'callbackSrv', + responseSrv: 'callbackSrv', + responseFnName: 'response', + errorHandlingSrv: 'errorHandlingSrv', + errorFnName: 'error', + }, + }, + script, + ); +} + +export function service_removed(file_path: string, config?: { ttl?: number }): Promise; + +export function service_removed(peer: FluencePeer, file_path: string, config?: { ttl?: number }): Promise; + +export function service_removed(...args: any) { + let script = ` + (xor + (seq + (seq + (seq + (seq + (seq + (seq + (call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-) + (call %init_peer_id% ("getDataSrv" "file_path") [] file_path) + ) + (call %init_peer_id% ("node_utils" "read_file") [file_path] file) + ) + (call %init_peer_id% ("single_module_srv" "create") [file.$.content.[0]!] created_service) + ) + (call %init_peer_id% ("single_module_srv" "remove") [created_service.$.service_id.[0]!]) + ) + (xor + (call %init_peer_id% (created_service.$.service_id.[0]! "greeting") ["test"] dontcare) + (null) + ) + ) + (xor + (call %init_peer_id% ("callbackSrv" "response") [%last_error%.$.message!]) + (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 1]) + ) + ) + (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2]) + ) + `; + return callFunction$$( + args, + { + functionName: 'service_removed', + arrow: { + tag: 'arrow', + domain: { + tag: 'labeledProduct', + fields: { + file_path: { + tag: 'scalar', + name: 'string', + }, + }, + }, + codomain: { + tag: 'unlabeledProduct', + items: [ + { + tag: 'scalar', + name: 'string', + }, + ], + }, + }, + names: { + relay: '-relay-', + getDataSrv: 'getDataSrv', + callbackSrv: 'callbackSrv', + responseSrv: 'callbackSrv', + responseFnName: 'response', + errorHandlingSrv: 'errorHandlingSrv', + errorFnName: 'error', + }, + }, + script, + ); +} + +export function list_services(file_path: string, config?: { ttl?: number }): Promise; + +export function list_services(peer: FluencePeer, file_path: string, config?: { ttl?: number }): Promise; + +export function list_services(...args: any) { + let script = ` + (xor + (seq + (seq + (seq + (seq + (seq + (seq + (seq + (call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-) + (call %init_peer_id% ("getDataSrv" "file_path") [] file_path) + ) + (call %init_peer_id% ("node_utils" "read_file") [file_path] file) + ) + (call %init_peer_id% ("single_module_srv" "create") [file.$.content.[0]!]) + ) + (call %init_peer_id% ("single_module_srv" "create") [file.$.content.[0]!]) + ) + (call %init_peer_id% ("single_module_srv" "create") [file.$.content.[0]!]) + ) + (call %init_peer_id% ("single_module_srv" "list") [] list) + ) + (xor + (call %init_peer_id% ("callbackSrv" "response") [list]) + (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 1]) + ) + ) + (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2]) + ) + `; + return callFunction$$( + args, + { + functionName: 'list_services', + arrow: { + tag: 'arrow', + domain: { + tag: 'labeledProduct', + fields: { + file_path: { + tag: 'scalar', + name: 'string', + }, + }, + }, + codomain: { + tag: 'unlabeledProduct', + items: [ + { + tag: 'array', + type: { + tag: 'scalar', + name: 'string', + }, + }, + ], + }, + }, + names: { + relay: '-relay-', + getDataSrv: 'getDataSrv', + callbackSrv: 'callbackSrv', + responseSrv: 'callbackSrv', + responseFnName: 'response', + errorHandlingSrv: 'errorHandlingSrv', + errorFnName: 'error', + }, + }, + script, + ); +} diff --git a/packages/fluence-js/src/__test__/integration/sigService.spec.ts b/packages/fluence-js/src/__test__/integration/sigService.spec.ts index 0914d6220..3fa6c9cc6 100644 --- a/packages/fluence-js/src/__test__/integration/sigService.spec.ts +++ b/packages/fluence-js/src/__test__/integration/sigService.spec.ts @@ -1,5 +1,6 @@ -import { Fluence, FluencePeer, KeyPair, setLogLevel } from '../../index'; -import { allowServiceFn, Sig } from '../../services'; +import { allowServiceFn } from '../../internal/builtins/securityGuard'; +import { FluencePeer, KeyPair } from '../../index'; +import { Sig } from '../../services'; import { registerSig, registerDataProvider, callSig } from '../_aqua/sig-tests'; let peer: FluencePeer; diff --git a/packages/fluence-js/src/__test__/integration/srv.spec.ts b/packages/fluence-js/src/__test__/integration/srv.spec.ts new file mode 100644 index 000000000..03500513e --- /dev/null +++ b/packages/fluence-js/src/__test__/integration/srv.spec.ts @@ -0,0 +1,73 @@ +import { Fluence, FluencePeer, KeyPair, setLogLevel } from '../../index'; + +import fs from 'fs/promises'; +import path from 'path'; +import { happy_path, service_removed, file_not_found, list_services, removing_non_exiting } from '../_aqua/srv-tests'; + +let peer: FluencePeer; + +describe('Srv service test suite', () => { + afterEach(async () => { + if (peer) { + await peer.stop(); + } + }); + + beforeEach(async () => { + peer = new FluencePeer(); + await peer.start(); + }); + + it('Use custom srv service, success path', async () => { + // arrange + const wasm = path.join(__dirname, './greeting.wasm'); + + // act + const res = await happy_path(peer, wasm); + + // assert + expect(res).toBe('Hi, test'); + }); + + it('List deployed services', async () => { + // arrange + const wasm = path.join(__dirname, './greeting.wasm'); + + // act + const res = await list_services(peer, wasm); + + // assert + expect(res).toHaveLength(3); + }); + + it('Correct error for removed services', async () => { + // arrange + const wasm = path.join(__dirname, './greeting.wasm'); + + // act + const res = await service_removed(peer, wasm); + + // assert + expect(res).toMatch('No handler has been registered for serviceId'); + }); + + it('Correct error for file not found', async () => { + // arrange + + // act + const res = await file_not_found(peer); + + // assert + expect(res).toMatch("ENOENT: no such file or directory, open '/random/incorrect/file'"); + }); + + it('4', async () => { + // arrange + + // act + const res = await removing_non_exiting(peer); + + // assert + expect(res).toMatch('Service with id random_id not found'); + }); +}); diff --git a/packages/fluence-js/src/__test__/unit/builtInHandler.spec.ts b/packages/fluence-js/src/__test__/unit/builtInHandler.spec.ts index 0e5cb1e46..d6722b3bb 100644 --- a/packages/fluence-js/src/__test__/unit/builtInHandler.spec.ts +++ b/packages/fluence-js/src/__test__/unit/builtInHandler.spec.ts @@ -2,8 +2,9 @@ import { CallParams, CallServiceData } from '../../internal/commonTypes'; import each from 'jest-each'; import { builtInServices } from '../../internal/builtins/common'; import { KeyPair } from '@fluencelabs/keypair'; -import { Sig, defaultSigGuard, allowServiceFn } from '../../internal/builtins/Sig'; +import { Sig, defaultSigGuard } from '../../internal/builtins/Sig'; import { toUint8Array } from 'js-base64'; +import { allowServiceFn } from '../../internal/builtins/securityGuard'; const a10b20 = `{ "a": 10, diff --git a/packages/fluence-js/src/internal/FluencePeer.ts b/packages/fluence-js/src/internal/FluencePeer.ts index 009769a79..427dc107a 100644 --- a/packages/fluence-js/src/internal/FluencePeer.ts +++ b/packages/fluence-js/src/internal/FluencePeer.ts @@ -23,16 +23,27 @@ import type { MultiaddrInput } from 'multiaddr'; import { CallServiceData, CallServiceResult, GenericCallServiceHandler, ResultCodes } from './commonTypes'; import { PeerIdB58 } from './commonTypes'; import { Particle, ParticleExecutionStage, ParticleQueueItem } from './Particle'; -import { throwIfNotSupported, dataToString, jsonify, MarineLoglevel, marineLogLevelToEnvs, isString } from './utils'; +import { + throwIfNotSupported, + dataToString, + jsonify, + MarineLoglevel, + marineLogLevelToEnvs, + isString, + ServiceError, +} from './utils'; import { concatMap, filter, pipe, Subject, tap } from 'rxjs'; import log from 'loglevel'; import { builtInServices } from './builtins/common'; import { defaultSigGuard, Sig } from './builtins/Sig'; import { registerSig } from './_aqua/services'; +import { registerSrv } from './_aqua/single-module-srv'; import Buffer from './Buffer'; import { isBrowser, isNode } from 'browser-or-node'; import { deserializeAvmResult, InterpreterResult, JSONValue, LogLevel, serializeAvmArgs } from '@fluencelabs/avm'; +import { NodeUtils, Srv } from './builtins/SingleModuleSrv'; +import { registerNodeUtils } from './_aqua/node-utils'; /** * Node of the Fluence network specified as a pair of node's multiaddr and it's peer id @@ -450,11 +461,15 @@ export class FluencePeer { this._classServices = { sig: new Sig(this._keyPair), + srv: new Srv(this), }; this._classServices.sig.securityGuard = defaultSigGuard(peerId); registerSig(this, this._classServices.sig); registerSig(this, peerId, this._classServices.sig); + registerSrv(this, this._classServices.srv); + registerNodeUtils(this, new NodeUtils(this)); + this._startParticleProcessing(); } @@ -497,6 +512,7 @@ export class FluencePeer { private _classServices?: { sig: Sig; + srv: Srv; }; private _containsService(serviceId: string): boolean { @@ -684,14 +700,21 @@ export class FluencePeer { }; this._execSingleCallRequest(req) - .catch( - (err): CallServiceResult => ({ + .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), diff --git a/packages/fluence-js/src/internal/_aqua/node-utils.ts b/packages/fluence-js/src/internal/_aqua/node-utils.ts new file mode 100644 index 000000000..0723e2831 --- /dev/null +++ b/packages/fluence-js/src/internal/_aqua/node-utils.ts @@ -0,0 +1,80 @@ +/** + * + * This file is auto-generated. Do not edit manually: changes may be erased. + * Generated by Aqua compiler: https://github.com/fluencelabs/aqua/. + * If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues + * Aqua version: 0.7.7-362 + * + */ +import { FluencePeer } from '../..'; +import type { CallParams$$ } from '../../internal/compilerSupport/v4'; +import { registerService$$ } from '../../internal/compilerSupport/v4'; + +// Services + +export interface NodeUtilsDef { + read_file: ( + path: string, + callParams: CallParams$$<'path'>, + ) => + | { content: string | null; error: string | null; success: boolean } + | Promise<{ content: string | null; error: string | null; success: boolean }>; +} +export function registerNodeUtils(service: NodeUtilsDef): void; +export function registerNodeUtils(serviceId: string, service: NodeUtilsDef): void; +export function registerNodeUtils(peer: FluencePeer, service: NodeUtilsDef): void; +export function registerNodeUtils(peer: FluencePeer, serviceId: string, service: NodeUtilsDef): void; + +export function registerNodeUtils(...args: any) { + registerService$$(args, { + defaultServiceId: 'node_utils', + functions: { + tag: 'labeledProduct', + fields: { + read_file: { + tag: 'arrow', + domain: { + tag: 'labeledProduct', + fields: { + path: { + tag: 'scalar', + name: 'string', + }, + }, + }, + codomain: { + tag: 'unlabeledProduct', + items: [ + { + tag: 'struct', + name: 'ReadFileResult', + fields: { + content: { + tag: 'option', + type: { + tag: 'scalar', + name: 'string', + }, + }, + error: { + tag: 'option', + type: { + tag: 'scalar', + name: 'string', + }, + }, + success: { + tag: 'scalar', + name: 'bool', + }, + }, + }, + ], + }, + }, + }, + }, + }); +} + +// Functions diff --git a/packages/fluence-js/src/internal/_aqua/services.ts b/packages/fluence-js/src/internal/_aqua/services.ts index e2e6487cf..89309c4f2 100644 --- a/packages/fluence-js/src/internal/_aqua/services.ts +++ b/packages/fluence-js/src/internal/_aqua/services.ts @@ -3,26 +3,27 @@ * This file is auto-generated. Do not edit manually: changes may be erased. * Generated by Aqua compiler: https://github.com/fluencelabs/aqua/. * If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues - * Aqua version: 0.7.2-314 + * Aqua version: 0.7.7-362 * */ -import { Fluence, FluencePeer } from '../../index'; -import { CallParams, callFunction, registerService } from '../../internal/compilerSupport/v3'; +import { FluencePeer } from '../..'; +import type { CallParams$$ } from '../../internal/compilerSupport/v4'; +import { registerService$$ } from '../../internal/compilerSupport/v4'; // Services export interface SigDef { - get_peer_id: (callParams: CallParams) => string | Promise; + get_peer_id: (callParams: CallParams$$) => string | Promise; sign: ( data: number[], - callParams: CallParams<'data'>, + callParams: CallParams$$<'data'>, ) => | { error: string | null; signature: number[] | null; success: boolean } | Promise<{ error: string | null; signature: number[] | null; success: boolean }>; verify: ( signature: number[], data: number[], - callParams: CallParams<'signature' | 'data'>, + callParams: CallParams$$<'signature' | 'data'>, ) => boolean | Promise; } export function registerSig(service: SigDef): void; @@ -31,7 +32,7 @@ export function registerSig(peer: FluencePeer, service: SigDef): void; export function registerSig(peer: FluencePeer, serviceId: string, service: SigDef): void; export function registerSig(...args: any) { - registerService(args, { + registerService$$(args, { defaultServiceId: 'sig', functions: { tag: 'labeledProduct', diff --git a/packages/fluence-js/src/internal/_aqua/single-module-srv.ts b/packages/fluence-js/src/internal/_aqua/single-module-srv.ts new file mode 100644 index 000000000..9788fc75e --- /dev/null +++ b/packages/fluence-js/src/internal/_aqua/single-module-srv.ts @@ -0,0 +1,137 @@ +/** + * + * This file is auto-generated. Do not edit manually: changes may be erased. + * Generated by Aqua compiler: https://github.com/fluencelabs/aqua/. + * If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues + * Aqua version: 0.7.7-362 + * + */ +import { FluencePeer } from '../..'; +import type { CallParams$$ } from '../../internal/compilerSupport/v4'; +import { registerService$$ } from '../../internal/compilerSupport/v4'; + +// Services + +export interface SrvDef { + create: ( + wasm_b64_content: string, + callParams: CallParams$$<'wasm_b64_content'>, + ) => + | { error: string | null; service_id: string | null; success: boolean } + | Promise<{ error: string | null; service_id: string | null; success: boolean }>; + list: (callParams: CallParams$$) => string[] | Promise; + remove: ( + service_id: string, + callParams: CallParams$$<'service_id'>, + ) => { error: string | null; success: boolean } | Promise<{ error: string | null; success: boolean }>; +} +export function registerSrv(service: SrvDef): void; +export function registerSrv(serviceId: string, service: SrvDef): void; +export function registerSrv(peer: FluencePeer, service: SrvDef): void; +export function registerSrv(peer: FluencePeer, serviceId: string, service: SrvDef): void; + +export function registerSrv(...args: any) { + registerService$$(args, { + defaultServiceId: 'single_module_srv', + functions: { + tag: 'labeledProduct', + fields: { + create: { + tag: 'arrow', + domain: { + tag: 'labeledProduct', + fields: { + wasm_b64_content: { + tag: 'scalar', + name: 'string', + }, + }, + }, + codomain: { + tag: 'unlabeledProduct', + items: [ + { + tag: 'struct', + name: 'ServiceCreationResult', + fields: { + error: { + tag: 'option', + type: { + tag: 'scalar', + name: 'string', + }, + }, + service_id: { + tag: 'option', + type: { + tag: 'scalar', + name: 'string', + }, + }, + success: { + tag: 'scalar', + name: 'bool', + }, + }, + }, + ], + }, + }, + list: { + tag: 'arrow', + domain: { + tag: 'nil', + }, + codomain: { + tag: 'unlabeledProduct', + items: [ + { + tag: 'array', + type: { + tag: 'scalar', + name: 'string', + }, + }, + ], + }, + }, + remove: { + tag: 'arrow', + domain: { + tag: 'labeledProduct', + fields: { + service_id: { + tag: 'scalar', + name: 'string', + }, + }, + }, + codomain: { + tag: 'unlabeledProduct', + items: [ + { + tag: 'struct', + name: 'RemoveResult', + fields: { + error: { + tag: 'option', + type: { + tag: 'scalar', + name: 'string', + }, + }, + success: { + tag: 'scalar', + name: 'bool', + }, + }, + }, + ], + }, + }, + }, + }, + }); +} + +// Functions diff --git a/packages/fluence-js/src/internal/builtins/Sig.ts b/packages/fluence-js/src/internal/builtins/Sig.ts index 02097f85f..dd2a99e8e 100644 --- a/packages/fluence-js/src/internal/builtins/Sig.ts +++ b/packages/fluence-js/src/internal/builtins/Sig.ts @@ -1,68 +1,10 @@ -import { SecurityTetraplet } from '@fluencelabs/avm'; import { CallParams, PeerIdB58 } from '../commonTypes'; import { KeyPair } from '@fluencelabs/keypair'; import { SigDef } from '../_aqua/services'; - -/** - * A predicate of call params for sig service's sign method which determines whether signing operation is allowed or not - */ -export type SigSecurityGuard = (params: CallParams<'data'>) => boolean; - -/** - * Only allow calls when tetraplet for 'data' argument satisfies the predicate - */ -export const allowTetraplet = (pred: (tetraplet: SecurityTetraplet) => boolean): SigSecurityGuard => { - return (params) => { - const t = params.tetraplets.data[0]; - return pred(t); - }; -}; - -/** - * Only allow data which comes from the specified serviceId and fnName - */ -export const allowServiceFn = (serviceId: string, fnName: string): SigSecurityGuard => { - return allowTetraplet((t) => { - return t.service_id === serviceId && t.function_name === fnName; - }); -}; - -/** - * Only allow data originated from the specified json_path - */ -export const allowExactJsonPath = (jsonPath: string): SigSecurityGuard => { - return allowTetraplet((t) => { - return t.json_path === jsonPath; - }); -}; - -/** - * Only allow signing when particle is initiated at the specified peer - */ -export const allowOnlyParticleOriginatedAt = (peerId: PeerIdB58): SigSecurityGuard => { - return (params) => { - return params.initPeerId === peerId; - }; -}; - -/** - * Only allow signing when all of the predicates are satisfied. - * Useful for predicates reuse - */ -export const and = (...predicates: SigSecurityGuard[]): SigSecurityGuard => { - return (params) => predicates.every((x) => x(params)); -}; - -/** - * Only allow signing when any of the predicates are satisfied. - * Useful for predicates reuse - */ -export const or = (...predicates: SigSecurityGuard[]): SigSecurityGuard => { - return (params) => predicates.some((x) => x(params)); -}; +import { allowOnlyParticleOriginatedAt, allowServiceFn, and, or, SecurityGuard } from './securityGuard'; export const defaultSigGuard = (peerId: PeerIdB58) => { - return and( + return and<'data'>( allowOnlyParticleOriginatedAt(peerId), or( allowServiceFn('trust-graph', 'get_trust_bytes'), @@ -83,9 +25,9 @@ export class Sig implements SigDef { } /** - * + * Configurable security guard for sign method */ - securityGuard: SigSecurityGuard = (params) => { + securityGuard: SecurityGuard<'data'> = (params) => { return true; }; diff --git a/packages/fluence-js/src/internal/builtins/SingleModuleSrv.ts b/packages/fluence-js/src/internal/builtins/SingleModuleSrv.ts new file mode 100644 index 000000000..c817bfe02 --- /dev/null +++ b/packages/fluence-js/src/internal/builtins/SingleModuleSrv.ts @@ -0,0 +1,125 @@ +import { v4 as uuidv4 } from 'uuid'; +import { SrvDef } from '../_aqua/single-module-srv'; +import { NodeUtilsDef } from '../_aqua/node-utils'; +import { FluencePeer } from '../FluencePeer'; +import { isNode } from 'browser-or-node'; +import { CallParams } from '../commonTypes'; +import { allowOnlyParticleOriginatedAt, SecurityGuard } from './securityGuard'; + +export const defaultGuard = (peer: FluencePeer) => { + return allowOnlyParticleOriginatedAt(peer.getStatus().peerId!); +}; + +export class Srv implements SrvDef { + private services: Set = new Set(); + + constructor(private peer: FluencePeer) {} + + securityGuard_create: SecurityGuard<'wasm_b64_content'> = defaultGuard(this.peer); + + async create(wasm_b64_content: string, callParams: CallParams<'wasm_b64_content'>) { + if (!this.securityGuard_create(callParams)) { + return { + success: false, + error: 'Security guard validation failed', + service_id: null, + }; + } + + 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); + this.services.add(newServiceId); + + return { + success: true, + service_id: newServiceId, + error: null, + }; + } catch (err: any) { + return { + success: true, + service_id: null, + error: err.message, + }; + } + } + + securityGuard_remove: SecurityGuard<'service_id'> = defaultGuard(this.peer); + + remove(service_id: string, callParams: CallParams<'service_id'>) { + if (!this.securityGuard_remove(callParams)) { + return { + success: false, + error: 'Security guard validation failed', + service_id: null, + }; + } + + if (!this.services.has(service_id)) { + return { + success: false, + error: `Service with id ${service_id} not found`, + }; + } + + this.peer.removeMarineService(service_id); + this.services.delete(service_id); + + return { + success: true, + error: null, + }; + } + + list() { + return Array.from(this.services.values()); + } +} + +export class NodeUtils implements NodeUtilsDef { + constructor(private peer: FluencePeer) {} + + securityGuard_readFile: SecurityGuard<'path'> = defaultGuard(this.peer); + + async read_file(path: string, callParams: CallParams<'path'>) { + 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/fluence-js/src/internal/builtins/securityGuard.ts b/packages/fluence-js/src/internal/builtins/securityGuard.ts new file mode 100644 index 000000000..a8d7c7721 --- /dev/null +++ b/packages/fluence-js/src/internal/builtins/securityGuard.ts @@ -0,0 +1,64 @@ +import { SecurityTetraplet } from '@fluencelabs/avm'; +import { CallParams, PeerIdB58 } from '../commonTypes'; + +type ArgName = string | null; + +/** + * A predicate of call params for sig service's sign method which determines whether signing operation is allowed or not + */ +export type SecurityGuard = (params: CallParams) => boolean; + +/** + * Only allow calls when tetraplet for 'data' argument satisfies the predicate + */ +export const allowTetraplet = ( + pred: (tetraplet: SecurityTetraplet) => boolean, +): SecurityGuard => { + return (params) => { + const t = params.tetraplets.data[0]; + return pred(t); + }; +}; + +/** + * Only allow data which comes from the specified serviceId and fnName + */ +export const allowServiceFn = (serviceId: string, fnName: string): SecurityGuard => { + return allowTetraplet((t) => { + return t.service_id === serviceId && t.function_name === fnName; + }); +}; + +/** + * Only allow data originated from the specified json_path + */ +export const allowExactJsonPath = (jsonPath: string): SecurityGuard => { + return allowTetraplet((t) => { + return t.json_path === jsonPath; + }); +}; + +/** + * Only allow signing when particle is initiated at the specified peer + */ +export const allowOnlyParticleOriginatedAt = (peerId: PeerIdB58): SecurityGuard => { + return (params) => { + return params.initPeerId === peerId; + }; +}; + +/** + * Only allow signing when all of the predicates are satisfied. + * Useful for predicates reuse + */ +export const and = (...predicates: SecurityGuard[]): SecurityGuard => { + return (params) => predicates.every((x) => x(params)); +}; + +/** + * Only allow signing when any of the predicates are satisfied. + * Useful for predicates reuse + */ +export const or = (...predicates: SecurityGuard[]): SecurityGuard => { + return (params) => predicates.some((x) => x(params)); +}; diff --git a/packages/fluence-js/src/internal/compilerSupport/v4.ts b/packages/fluence-js/src/internal/compilerSupport/v4.ts index fbafa1242..6d747bf96 100644 --- a/packages/fluence-js/src/internal/compilerSupport/v4.ts +++ b/packages/fluence-js/src/internal/compilerSupport/v4.ts @@ -36,6 +36,6 @@ export { StructType as StructType$$, TopType as TopType$$, UnlabeledProductType as UnlabeledProductType$$, -} from './v3impl/interface'; -export { callFunction as callFunction$$ } from './v3impl/callFunction'; -export { registerService as registerService$$ } from './v3impl/registerService'; + callFunction as callFunction$$, + registerService as registerService$$, +} from './v3'; diff --git a/packages/fluence-js/src/internal/utils.ts b/packages/fluence-js/src/internal/utils.ts index 556697b92..e0adef2b8 100644 --- a/packages/fluence-js/src/internal/utils.ts +++ b/packages/fluence-js/src/internal/utils.ts @@ -177,3 +177,11 @@ export const marineLogLevelToEnvs = (marineLogLevel: MarineLoglevel | undefined) 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); + } +}