Update JS SDK API to the new version (#61)

* FluenceClient renamed to FluencePeer.
* Using Aqua compiler is now the recommended way for all interaction with the network, including services registration and sending requests
* Old API (sendParticle etc) has been removed
* Opaque seed format replaced with 32 byte ed25519 private key. KeyPair introduced
* Documentation update
This commit is contained in:
Pavel 2021-09-08 12:42:30 +03:00 committed by GitHub
parent c7ab9d56ee
commit 6436cd5684
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1880 additions and 2773 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ lerna-debug.log*
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
bundle/
docs/
# Dependency directories
node_modules/

102
README.md
View File

@ -2,105 +2,17 @@
[![npm](https://img.shields.io/npm/v/@fluencelabs/fluence)](https://www.npmjs.com/package/@fluencelabs/fluence)
Official SDK for building web-based applications for Fluence
## About Fluence
Fluence is an open application platform where apps can build on each other, share data and users
| Layer | Tech | Scale | State | Based on |
| :-------------------: | :-------------------------------------------------------------------------------------------------------------------------------: | :------------------------------: | :-------------------------------: | :-----------------------------------------------------------------------------------------------------------: |
| Execution | [FCE](https://github.com/fluencelabs/fce) | Single peer | Disk, network, external processes | Wasm, [IT](https://github.com/fluencelabs/interface-types), [Wasmer\*](https://github.com/fluencelabs/wasmer) |
| Composition | [Aquamarine](https://github.com/fluencelabs/aquamarine) | Involved peers | Results and signatures | ⇅, π-calculus |
| Topology | [TrustGraph](https://github.com/fluencelabs/fluence/tree/master/trust-graph), [DHT\*](https://github.com/fluencelabs/rust-libp2p) | Distributed with Kademlia\* algo | Actual state of the network | [libp2p](https://github.com/libp2p/rust-libp2p) |
| Security & Accounting | Blockchain | Whole network | Licenses & payments | substrate? |
<img alt="aquamarine scheme" align="center" src="doc/stack.png"/>
## Installation
With npm
```bash
npm install @fluencelabs/fluence
```
With yarn
```bash
yarn add @fluencelabs/fluence
```
Official SDK providing javascript-based implementation of the Fluence Peer.
## Getting started
Pick a node to connect to the Fluence network. The easiest way to do so is by using [fluence-network-environment](https://github.com/fluencelabs/fluence-network-environment) package
To start developing applications with JS SDK refer to the official [gitbook page](https://doc.fluence.dev/docs/js-sdk)
```typescript
import { dev } from '@fluencelabs/fluence-network-environment';
## Contributing
export const relayNode = dev[0];
```
While the project is still in the early stages of development, you are welcome to track progress and contribute. As the project is undergoing rapid changes, interested contributors should contact the team before embarking on larger pieces of work. All contributors should consult with and agree to our [basic contributing rules](CONTRIBUTING.md).
Initialize client
```typescript
import { createClient, FluenceClient } from '@fluencelabs/fluence';
const client = await createClient(relayNode);
```
Respond to service function calls
```typescript
subscribeToEvent(client, 'helloService', 'helloFunction', (args) => {
const [networkInfo] = args;
console.log(networkInfo);
});
```
Make a particle
```typescript
const particle = new Particle(
`
(seq
(call myRelay ("peer" "identify") [] result)
(call %init_peer_id% ("helloService" "helloFunction") [result])
)`,
{
myRelay: client.relayPeerId,
},
);
```
Send it to the network
```typescript
await sendParticle(client, particle);
```
Observe the result in browser console
```json
{
"external_addresses": ["/ip4/1.2.3.4/tcp/7777", "/dns4/dev.fluence.dev/tcp/19002"]
}
```
## Documentation
Guide on building applications: [doc.fluence.dev](https://doc.fluence.dev/docs/tutorials_tutorials/building-a-frontend-with-js-sdk)
Sample applications:
- [FluentPad](https://github.com/fluencelabs/fluent-pad): a collaborative text editor with users online status synchronization
- [Examples](https://github.com/fluencelabs/examples): examples of using the Aqua programming language
About [Fluence](https://fluence.network/)
## Developing
### Setting up Dev
### Setting up dev environment
Install node packages
@ -140,10 +52,6 @@ To run all tests
npm run test:all
```
## Contributing
While the project is still in the early stages of development, you are welcome to track progress and contribute. As the project is undergoing rapid changes, interested contributors should contact the team before embarking on larger pieces of work. All contributors should consult with and agree to our [basic contributing rules](CONTRIBUTING.md).
## License
[Apache 2.0](LICENSE)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

1957
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,14 +13,15 @@
"test:node:all": "jest --env=node",
"test:node:unit": "jest --env=node --testPathPattern=src/__test__/unit",
"test:node:integration": "jest --env=node --testPathPattern=src/__test__/integration",
"build": "tsc"
"build": "tsc",
"build:docs": "typedoc --out docs --excludePrivate --hideBreadcrumbs --publicPath js-sdk/6_reference/ src/index.ts"
},
"repository": "https://github.com/fluencelabs/fluence-js",
"author": "Fluence Labs",
"license": "Apache-2.0",
"dependencies": {
"@chainsafe/libp2p-noise": "4.0.0",
"@fluencelabs/avm": "0.14.3",
"@fluencelabs/avm": "0.14.4",
"async": "3.2.0",
"base64-js": "1.5.1",
"bs58": "4.0.1",
@ -33,6 +34,7 @@
"libp2p-websockets": "0.16.1",
"loglevel": "1.7.0",
"multiaddr": "10.0.0",
"noble-ed25519": "^1.2.5",
"peer-id": "0.15.3",
"uuid": "8.3.0"
},
@ -40,6 +42,8 @@
"@types/jest": "^26.0.22",
"jest": "^26.6.3",
"ts-jest": "^26.5.4",
"typescript": "^3.9.5"
"typedoc": "^0.21.9",
"typedoc-plugin-markdown": "^3.10.4",
"typescript": "^4.0.0"
}
}

View File

@ -1,150 +0,0 @@
import log from 'loglevel';
import { Multiaddr } from 'multiaddr';
import PeerId, { isPeerId } from 'peer-id';
import { CallServiceHandler } from './internal/CallServiceHandler';
import { ClientImpl } from './internal/ClientImpl';
import { PeerIdB58 } from './internal/commonTypes';
import { FluenceConnectionOptions } from './internal/FluenceConnection';
import { generatePeerId, seedToPeerId } from './internal/peerIdUtils';
import { RequestFlow } from './internal/RequestFlow';
import { RequestFlowBuilder } from './internal/RequestFlowBuilder';
/**
* The class represents interface to Fluence Platform. To create a client use @see {@link createClient} function.
*/
export interface FluenceClient {
/**
* { string } Gets the base58 representation of the current peer id. Read only
*/
readonly relayPeerId: PeerIdB58 | undefined;
/**
* { string } Gets the base58 representation of the connected relay's peer id. Read only
*/
readonly selfPeerId: PeerIdB58;
/**
* { string } True if the client is connected to network. False otherwise. Read only
*/
readonly isConnected: boolean;
/**
* The base handler which is used by every RequestFlow executed by this FluenceClient.
* Please note, that the handler is combined with the handler from RequestFlow before the execution occures.
* After this combination, middlewares from RequestFlow are executed before client handler's middlewares.
*/
readonly callServiceHandler: CallServiceHandler;
/**
* Disconnects the client from the network
*/
disconnect(): Promise<void>;
/**
* Establish a connection to the node. If the connection is already established, disconnect and reregister all services in a new connection.
*
* @param multiaddr
*/
connect(multiaddr: string | Multiaddr): Promise<void>;
/**
* Initiates RequestFlow execution @see { @link RequestFlow }
* @param { RequestFlow } [ request ] - RequestFlow to start the execution of
*/
initiateFlow(request: RequestFlow): Promise<void>;
}
type Node = {
peerId: string;
multiaddr: string;
};
/**
* Creates a Fluence client. If the `connectTo` is specified connects the client to the network
* @param { string | Multiaddr | Node } [connectTo] - Node in Fluence network to connect to. If not specified client will not be connected to the n
* @param { PeerId | string } [peerIdOrSeed] - The Peer Id of the created client. Specified either as PeerId structure or as seed string. Will be generated randomly if not specified
* @param { FluenceConnectionOptions } [options] - additional configuraton options for Fluence Connection made with the client
* @returns { Promise<FluenceClient> } Promise which will be resolved with the created FluenceClient
*/
export const createClient = async (
connectTo?: string | Multiaddr | Node,
peerIdOrSeed?: PeerId | string,
options?: FluenceConnectionOptions,
): Promise<FluenceClient> => {
let peerId;
if (!peerIdOrSeed) {
peerId = await generatePeerId();
} else if (isPeerId(peerIdOrSeed)) {
// keep unchanged
peerId = peerIdOrSeed;
} else {
// peerId is string, therefore seed
peerId = await seedToPeerId(peerIdOrSeed);
}
const client = new ClientImpl(peerId);
await client.initAirInterpreter();
if (connectTo) {
let theAddress: Multiaddr;
let fromNode = (connectTo as any).multiaddr;
if (fromNode) {
theAddress = new Multiaddr(fromNode);
} else {
theAddress = new Multiaddr(connectTo as string);
}
await client.connect(theAddress, options);
if (options?.skipCheckConnection) {
if (!(await checkConnection(client, options.checkConnectionTTL))) {
throw new Error(
'Connection check failed. Check if the node is working or try to connect to another node',
);
}
}
}
return client;
};
/**
* Checks the network connection by sending a ping-like request to relat node
* @param { FluenceClient } client - The Fluence Client instance.
*/
export const checkConnection = async (client: FluenceClient, ttl?: number): Promise<boolean> => {
if (!client.isConnected) {
return false;
}
const msg = Math.random().toString(36).substring(7);
const callbackFn = 'checkConnection';
const callbackService = '_callback';
const [request, promise] = new RequestFlowBuilder()
.withRawScript(
`(seq
(call init_relay ("op" "identity") [msg] result)
(call %init_peer_id% ("${callbackService}" "${callbackFn}") [result])
)`,
)
.withTTL(ttl)
.withVariables({
msg,
})
.buildAsFetch<[string]>(callbackService, callbackFn);
await client.initiateFlow(request);
try {
const [result] = await promise;
if (result != msg) {
log.warn("unexpected behavior. 'identity' must return the passed arguments.");
}
return true;
} catch (e) {
log.error('Error on establishing connection: ', e);
return false;
}
};

View File

@ -0,0 +1,50 @@
import { FluencePeer } from '../../index';
import { RequestFlowBuilder } from '../../internal/RequestFlowBuilder';
const peer = new FluencePeer();
describe('Avm spec', () => {
afterEach(async () => {
if (peer) {
await peer.uninit();
}
});
it('Par execution should work', async () => {
// arrange
await peer.init();
let request;
const promise = new Promise<string[]>((resolve) => {
let res = [];
request = new RequestFlowBuilder()
.withRawScript(
`
(seq
(par
(call %init_peer_id% ("print" "print") ["1"])
(null)
)
(call %init_peer_id% ("print" "print") ["2"])
)
`,
)
.configHandler((h) => {
h.onEvent('print', 'print', async (args) => {
res.push(args[0]);
if (res.length == 2) {
resolve(res);
}
});
})
.build();
});
// act
await peer.internals.initiateFlow(request);
const res = await promise;
// assert
expect(res).toStrictEqual(['1', '2']);
});
});

View File

@ -1,122 +0,0 @@
import {
addBlueprint,
addScript,
createService,
getBlueprints,
getInterfaces,
getModules,
removeScript,
uploadModule,
} from '../../internal/builtins';
import { ModuleConfig } from '../../internal/moduleConfig';
import { createClient, FluenceClient } from '../../FluenceClient';
import { nodes } from '../connection';
let client: FluenceClient;
describe('Builtins usage suite', () => {
afterEach(async () => {
if (client) {
await client.disconnect();
}
});
jest.setTimeout(10000);
it('get_modules', async function () {
client = await createClient(nodes[0].multiaddr);
let modulesList = await getModules(client);
expect(modulesList).not.toBeUndefined;
});
it('get_interfaces', async function () {
client = await createClient(nodes[0].multiaddr);
let interfaces = await getInterfaces(client);
expect(interfaces).not.toBeUndefined;
});
it('get_blueprints', async function () {
client = await createClient(nodes[0].multiaddr);
let bpList = await getBlueprints(client);
expect(bpList).not.toBeUndefined;
});
it('upload_modules', async function () {
client = await createClient(nodes[0].multiaddr);
let config: ModuleConfig = {
name: 'test_broken_module',
mem_pages_count: 100,
logger_enabled: true,
wasi: {
envs: { a: 'b' },
preopened_files: ['a', 'b'],
mapped_dirs: { c: 'd' },
},
mounted_binaries: { e: 'f' },
};
let base64 = 'MjNy';
await uploadModule(client, 'test_broken_module', base64, config, 10000);
});
it('add_blueprint', async function () {
client = await createClient(nodes[0].multiaddr);
let bpId = 'some';
let bpIdReturned = await addBlueprint(client, 'test_broken_blueprint', ['test_broken_module'], bpId);
let allBps = await getBlueprints(client);
const allBpIds = allBps.map((x) => x.id);
expect(allBpIds).toContain(bpIdReturned);
});
it('create broken blueprint', async function () {
client = await createClient(nodes[0].multiaddr);
let promise = createService(client, 'test_broken_blueprint');
await expect(promise).rejects.toMatchObject({
msg: expect.stringContaining("Blueprint 'test_broken_blueprint' wasn't found"),
instruction: expect.stringContaining('blueprint_id'),
});
});
it('add and remove script', async function () {
client = await createClient(nodes[0].multiaddr);
let script = `
(seq
(call "${client.relayPeerId}" ("op" "identity") [])
(call "${client.selfPeerId}" ("test" "test1") ["1" "2" "3"] result)
)
`;
let resMakingPromise = new Promise((resolve) => {
client.callServiceHandler.on('test', 'test1', (args, _) => {
resolve([...args]);
return {};
});
});
let scriptId = await addScript(client, script);
await resMakingPromise
.then((args) => {
expect(args as string[]).toEqual(['1', '2', '3']);
})
.finally(() => {
removeScript(client, scriptId);
});
expect(scriptId).not.toBeUndefined;
});
});

View File

@ -0,0 +1,161 @@
import { FluencePeer } from '../../..';
import { RequestFlowBuilder } from '../../../internal/RequestFlowBuilder';
import { callMeBack, registerHelloWorld } from './gen1';
describe('Compiler support infrastructure tests', () => {
it('Compiled code for function should work', async () => {
// arrange
await FluencePeer.default.init();
// act
const res = new Promise((resolve) => {
callMeBack((arg0, arg1, params) => {
resolve({
arg0: arg0,
arg1: arg1,
arg0Tetraplet: params.tetraplets.arg0[0], // completion should work here
arg1Tetraplet: params.tetraplets.arg1[0], // completion should work here
});
});
});
// assert
expect(await res).toMatchObject({
arg0: 'hello, world',
arg1: 42,
arg0Tetraplet: {
function_name: '',
json_path: '',
// peer_pk: '12D3KooWMwDDVRPEn5YGrN5LvVFLjNuBmokaeKfpLUgxsSkqRwwv',
service_id: '',
},
arg1Tetraplet: {
function_name: '',
json_path: '',
// peer_pk: '12D3KooWMwDDVRPEn5YGrN5LvVFLjNuBmokaeKfpLUgxsSkqRwwv',
service_id: '',
},
});
await FluencePeer.default.uninit();
});
it('Compiled code for service should work', async () => {
// arrange
await FluencePeer.default.init();
// act
const helloPromise = new Promise((resolve) => {
registerHelloWorld('hello_world', {
sayHello: (s, params) => {
const tetrapelt = params.tetraplets.s; // completion should work here
resolve(s);
},
getNumber: (params) => {
// ctx.tetraplets should be {}
return 42;
},
});
});
const [request, getNumberPromise] = new RequestFlowBuilder()
.withRawScript(
`(seq
(seq
(call %init_peer_id% ("hello_world" "sayHello") ["hello world!"])
(call %init_peer_id% ("hello_world" "getNumber") [] result)
)
(call %init_peer_id% ("callback" "callback") [result])
)`,
)
.buildAsFetch<[string]>('callback', 'callback');
await FluencePeer.default.internals.initiateFlow(request);
// assert
expect(await helloPromise).toBe('hello world!');
expect(await getNumberPromise).toStrictEqual([42]);
await FluencePeer.default.uninit();
});
it('Compiled code for function should work with another peer', async () => {
// arrange
const peer = new FluencePeer();
await peer.init();
// act
const res = new Promise((resolve) => {
callMeBack(peer, (arg0, arg1, params) => {
resolve({
arg0: arg0,
arg1: arg1,
arg0Tetraplet: params.tetraplets.arg0[0], // completion should work here
arg1Tetraplet: params.tetraplets.arg1[0], // completion should work here
});
});
});
// assert
expect(await res).toMatchObject({
arg0: 'hello, world',
arg1: 42,
arg0Tetraplet: {
function_name: '',
json_path: '',
// peer_pk: '12D3KooWMwDDVRPEn5YGrN5LvVFLjNuBmokaeKfpLUgxsSkqRwwv',
service_id: '',
},
arg1Tetraplet: {
function_name: '',
json_path: '',
// peer_pk: '12D3KooWMwDDVRPEn5YGrN5LvVFLjNuBmokaeKfpLUgxsSkqRwwv',
service_id: '',
},
});
await peer.uninit();
});
it('Compiled code for service should work another peer', async () => {
// arrange
const peer = new FluencePeer();
await peer.init();
// act
const helloPromise = new Promise((resolve) => {
registerHelloWorld(peer, 'hello_world', {
sayHello: (s, params) => {
const tetrapelt = params.tetraplets.s; // completion should work here
resolve(s);
},
getNumber: (params) => {
// ctx.tetraplets should be {}
return 42;
},
});
});
const [request, getNumberPromise] = new RequestFlowBuilder()
.withRawScript(
`(seq
(seq
(call %init_peer_id% ("hello_world" "sayHello") ["hello world!"])
(call %init_peer_id% ("hello_world" "getNumber") [] result)
)
(call %init_peer_id% ("callback" "callback") [result])
)`,
)
.buildAsFetch<[string]>('callback', 'callback');
await peer.internals.initiateFlow(request);
// assert
expect(await helloPromise).toBe('hello world!');
expect(await getNumberPromise).toStrictEqual([42]);
await peer.uninit();
});
});

View File

@ -0,0 +1,177 @@
import { ResultCodes, RequestFlow, RequestFlowBuilder, CallParams } from '../../../internal/compilerSupport/v1';
import { FluencePeer } from '../../../index';
/*
-- file to generate functions below from
service HelloWorld("default"):
sayHello(s: string)
getNumber() -> i32
func callMeBack(callback: string, i32 -> ()):
callback("hello, world", 42)
*/
/**
*
* 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.2.2-SNAPSHOT
*
*/
// Services
export interface HelloWorldDef {
getNumber: (callParams: CallParams<null>) => number;
sayHello: (s: string, callParams: CallParams<'s'>) => void;
}
export function registerHelloWorld(service: HelloWorldDef): void;
export function registerHelloWorld(serviceId: string, service: HelloWorldDef): void;
export function registerHelloWorld(peer: FluencePeer, service: HelloWorldDef): void;
export function registerHelloWorld(peer: FluencePeer, serviceId: string, service: HelloWorldDef): void;
export function registerHelloWorld(...args) {
let peer: FluencePeer;
let serviceId;
let service;
if (args[0] instanceof FluencePeer) {
peer = args[0];
} else {
peer = FluencePeer.default;
}
if (typeof args[0] === 'string') {
serviceId = args[0];
} else if (typeof args[1] === 'string') {
serviceId = args[1];
} else {
serviceId = 'default';
}
if (!(args[0] instanceof FluencePeer) && typeof args[0] === 'object') {
service = args[0];
} else if (typeof args[1] === 'object') {
service = args[1];
} else {
service = args[2];
}
peer.internals.callServiceHandler.use((req, resp, next) => {
if (req.serviceId !== serviceId) {
next();
return;
}
if (req.fnName === 'getNumber') {
const callParams = {
...req.particleContext,
tetraplets: {},
};
resp.retCode = ResultCodes.success;
resp.result = service.getNumber(callParams);
}
if (req.fnName === 'sayHello') {
const callParams = {
...req.particleContext,
tetraplets: {
s: req.tetraplets[0],
},
};
resp.retCode = ResultCodes.success;
service.sayHello(req.args[0], callParams);
resp.result = {};
}
next();
});
}
// Functions
export function callMeBack(
callback: (arg0: string, arg1: number, callParams: CallParams<'arg0' | 'arg1'>) => void,
config?: { ttl?: number },
): Promise<void>;
export function callMeBack(
peer: FluencePeer,
callback: (arg0: string, arg1: number, callParams: CallParams<'arg0' | 'arg1'>) => void,
config?: { ttl?: number },
): Promise<void>;
export function callMeBack(...args) {
let peer: FluencePeer;
let callback;
let config;
if (args[0] instanceof FluencePeer) {
peer = args[0];
callback = args[1];
config = args[2];
} else {
peer = FluencePeer.default;
callback = args[0];
config = args[1];
}
let request: RequestFlow;
const promise = new Promise<void>((resolve, reject) => {
const r = new RequestFlowBuilder()
.disableInjections()
.withRawScript(
`
(xor
(seq
(call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-)
(xor
(call %init_peer_id% ("callbackSrv" "callback") ["hello, world" 42])
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 1])
)
)
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2])
)
`,
)
.configHandler((h) => {
h.on('getDataSrv', '-relay-', () => {
return peer.connectionInfo.connectedRelay || null;
});
h.use((req, resp, next) => {
if (req.serviceId === 'callbackSrv' && req.fnName === 'callback') {
const callParams = {
...req.particleContext,
tetraplets: {
arg0: req.tetraplets[0],
arg1: req.tetraplets[1],
},
};
resp.retCode = ResultCodes.success;
callback(req.args[0], req.args[1], callParams);
resp.result = {};
}
next();
});
h.onEvent('callbackSrv', 'response', (args) => {});
h.onEvent('errorHandlingSrv', 'error', (args) => {
const [err] = args;
reject(err);
});
})
.handleScriptError(reject)
.handleTimeout(() => {
reject('Request timed out for callMeBack');
});
if (config && config.ttl) {
r.withTTL(config.ttl);
}
request = r.build();
});
peer.internals.initiateFlow(request!);
return Promise.race([promise, Promise.resolve()]);
}

View File

@ -1,126 +0,0 @@
import { Particle, sendParticle, registerServiceFunction, subscribeToEvent, sendParticleAsFetch } from '../../api';
import { FluenceClient, createClient } from '../../FluenceClient';
import { nodes } from '../connection';
let client: FluenceClient;
describe('Legacy api suite', () => {
afterEach(async () => {
if (client) {
await client.disconnect();
}
});
it('sendParticle', async () => {
client = await createClient(nodes[0]);
const result = new Promise((resolve) => {
subscribeToEvent(client, 'callback', 'callback', (args) => {
resolve(args[0]);
});
});
const script = `(seq
(call init_relay ("op" "identity") [])
(call %init_peer_id% ("callback" "callback") [arg])
)`;
const data = {
arg: 'hello world!',
};
await sendParticle(client, new Particle(script, data, 7000));
expect(await result).toBe('hello world!');
});
it('sendParticle Error', async () => {
client = await createClient(nodes[0]);
const script = `
(call init_relay ("incorrect" "service") [])
`;
const promise = new Promise((resolve, reject) => {
sendParticle(client, new Particle(script), reject);
});
await expect(promise).rejects.toMatchObject({
msg: expect.stringContaining("Service with id 'incorrect' not found"),
instruction: expect.stringContaining('incorrect'),
});
});
it('sendParticleAsFetch', async () => {
client = await createClient(nodes[0]);
const script = `(seq
(call init_relay ("op" "identity") [])
(call %init_peer_id% ("service" "fn") [arg])
)`;
const data = {
arg: 'hello world!',
};
const [result] = await sendParticleAsFetch<[string]>(client, new Particle(script, data, 7000), 'fn', 'service');
expect(result).toBe('hello world!');
});
it('sendParticleAsFetch Error', async () => {
client = await createClient(nodes[0]);
const script = `
(call init_relay ("incorrect" "service") [])
`;
const promise = sendParticleAsFetch<[string]>(client, new Particle(script), 'fn', 'service');
await expect(promise).rejects.toMatchObject({
msg: expect.stringContaining("Service with id 'incorrect' not found"),
instruction: expect.stringContaining('incorrect'),
});
});
it('registerServiceFunction', async () => {
client = await createClient(nodes[0]);
registerServiceFunction(client, 'service', 'fn', (args) => {
return { res: args[0] + ' world!' };
});
const script = `(seq
(call %init_peer_id% ("service" "fn") ["hello"] result)
(call %init_peer_id% ("callback" "callback") [result])
)`;
const [result] = await sendParticleAsFetch<[string]>(
client,
new Particle(script, {}, 7000),
'callback',
'callback',
);
expect(result).toEqual({ res: 'hello world!' });
});
it('subscribeToEvent', async () => {
client = await createClient(nodes[0]);
const promise = new Promise((resolve) => {
subscribeToEvent(client, 'service', 'fn', (args) => {
resolve(args[0] + ' world!');
});
});
const script = `
(call %init_peer_id% ("service" "fn") ["hello"])
`;
await sendParticle(client, new Particle(script, {}, 7000));
const result = await promise;
expect(result).toBe('hello world!');
});
});

View File

@ -1,22 +1,22 @@
import { checkConnection, createClient, FluenceClient } from '../../FluenceClient';
import { Multiaddr } from 'multiaddr';
import { nodes } from '../connection';
import { RequestFlowBuilder } from '../../internal/RequestFlowBuilder';
import log from 'loglevel';
import { FluencePeer } from '../../index';
import { checkConnection } from '../../internal/utils';
let client: FluenceClient;
const peer = new FluencePeer();
describe('Typescript usage suite', () => {
afterEach(async () => {
if (client) {
await client.disconnect();
if (peer) {
await peer.uninit();
}
});
it('should make a call through network', async () => {
// arrange
client = await createClient();
await client.connect(nodes[0].multiaddr);
await peer.init({ connectTo: nodes[0] });
// act
const [request, promise] = new RequestFlowBuilder()
@ -27,7 +27,8 @@ describe('Typescript usage suite', () => {
)`,
)
.buildAsFetch<[string]>('callback', 'callback');
await client.initiateFlow(request);
await peer.internals.initiateFlow(request);
console.log(request.getParticle().script);
// assert
const [result] = await promise;
@ -35,30 +36,30 @@ describe('Typescript usage suite', () => {
});
it('check connection should work', async function () {
client = await createClient();
await client.connect(nodes[0].multiaddr);
await peer.init({ connectTo: nodes[0] });
let isConnected = await checkConnection(client);
let isConnected = await checkConnection(peer);
expect(isConnected).toEqual(true);
});
it('check connection should work with ttl', async function () {
client = await createClient();
await client.connect(nodes[0].multiaddr);
await peer.init({ connectTo: nodes[0] });
let isConnected = await checkConnection(client, 10000);
let isConnected = await checkConnection(peer, 10000);
expect(isConnected).toEqual(true);
});
it('two clients should work inside the same time browser', async () => {
// arrange
const client1 = await createClient(nodes[0].multiaddr);
const client2 = await createClient(nodes[0].multiaddr);
const peer1 = new FluencePeer();
await peer1.init({ connectTo: nodes[0] });
const peer2 = new FluencePeer();
await peer2.init({ connectTo: nodes[0] });
let resMakingPromise = new Promise((resolve) => {
client2.callServiceHandler.onEvent('test', 'test', (args, _) => {
peer2.internals.callServiceHandler.onEvent('test', 'test', (args, _) => {
resolve([...args]);
return {};
});
@ -66,8 +67,8 @@ describe('Typescript usage suite', () => {
let script = `
(seq
(call "${client1.relayPeerId}" ("op" "identity") [])
(call "${client2.selfPeerId}" ("test" "test") [a b c d])
(call "${peer1.connectionInfo.connectedRelay}" ("op" "identity") [])
(call "${peer2.connectionInfo.selfPeerId}" ("test" "test") [a b c d])
)
`;
@ -77,23 +78,23 @@ describe('Typescript usage suite', () => {
data.set('c', 'some c');
data.set('d', 'some d');
await client1.initiateFlow(new RequestFlowBuilder().withRawScript(script).withVariables(data).build());
await peer1.internals.initiateFlow(new RequestFlowBuilder().withRawScript(script).withVariables(data).build());
let res = await resMakingPromise;
expect(res).toEqual(['some a', 'some b', 'some c', 'some d']);
await client1.disconnect();
await client2.disconnect();
await peer1.uninit();
await peer2.uninit();
});
describe('should make connection to network', () => {
it('address as string', async () => {
// arrange
const addr = nodes[0].multiaddr;
const addr = nodes[0];
// act
client = await createClient(addr);
const isConnected = await checkConnection(client);
await peer.init({ connectTo: addr });
const isConnected = await checkConnection(peer);
// assert
expect(isConnected).toBeTruthy;
@ -104,8 +105,8 @@ describe('Typescript usage suite', () => {
const addr = new Multiaddr(nodes[0].multiaddr);
// act
client = await createClient(addr);
const isConnected = await checkConnection(client);
await peer.init({ connectTo: addr });
const isConnected = await checkConnection(peer);
// assert
expect(isConnected).toBeTruthy;
@ -116,8 +117,8 @@ describe('Typescript usage suite', () => {
const addr = nodes[0];
// act
client = await createClient(addr);
const isConnected = await checkConnection(client);
await peer.init({ connectTo: addr });
const isConnected = await checkConnection(peer);
// assert
expect(isConnected).toBeTruthy;
@ -125,11 +126,11 @@ describe('Typescript usage suite', () => {
it('peerid as peer id', async () => {
// arrange
const addr = nodes[0].multiaddr;
const addr = nodes[0];
// act
client = await createClient(addr);
const isConnected = await checkConnection(client);
await peer.init({ connectTo: addr });
const isConnected = await checkConnection(peer);
// assert
expect(isConnected).toBeTruthy;
@ -137,11 +138,11 @@ describe('Typescript usage suite', () => {
it('peerid as seed', async () => {
// arrange
const addr = nodes[0].multiaddr;
const addr = nodes[0];
// act
client = await createClient(addr);
const isConnected = await checkConnection(client);
await peer.init({ connectTo: addr });
const isConnected = await checkConnection(peer);
// assert
expect(isConnected).toBeTruthy;
@ -149,11 +150,11 @@ describe('Typescript usage suite', () => {
it('With connection options: dialTimeout', async () => {
// arrange
const addr = nodes[0].multiaddr;
const addr = nodes[0];
// act
client = await createClient(addr, undefined, { dialTimeout: 100000 });
const isConnected = await checkConnection(client);
await peer.init({ connectTo: addr, dialTimeoutMs: 100000 });
const isConnected = await checkConnection(peer);
// assert
expect(isConnected).toBeTruthy;
@ -161,11 +162,11 @@ describe('Typescript usage suite', () => {
it('With connection options: skipCheckConnection', async () => {
// arrange
const addr = nodes[0].multiaddr;
const addr = nodes[0];
// act
client = await createClient(addr, undefined, { skipCheckConnection: true });
const isConnected = await checkConnection(client);
await peer.init({ connectTo: addr, skipCheckConnection: true });
const isConnected = await checkConnection(peer);
// assert
expect(isConnected).toBeTruthy;
@ -173,11 +174,11 @@ describe('Typescript usage suite', () => {
it('With connection options: checkConnectionTTL', async () => {
// arrange
const addr = nodes[0].multiaddr;
const addr = nodes[0];
// act
client = await createClient(addr, undefined, { checkConnectionTTL: 1000 });
const isConnected = await checkConnection(client);
await peer.init({ connectTo: addr, checkConnectionTimeoutMs: 1000 });
const isConnected = await checkConnection(peer);
// assert
expect(isConnected).toBeTruthy;
@ -198,8 +199,8 @@ describe('Typescript usage suite', () => {
.buildWithErrorHandling();
// act
client = await createClient(nodes[0].multiaddr);
await client.initiateFlow(request);
await peer.init({ connectTo: nodes[0] });
await peer.internals.initiateFlow(request);
// assert
await expect(promise).rejects.toMatchObject({
@ -225,19 +226,19 @@ describe('Typescript usage suite', () => {
.buildWithErrorHandling();
// act
client = await createClient();
await client.initiateFlow(request);
await peer.init();
await peer.internals.initiateFlow(request);
// assert
await expect(promise).rejects.toMatch('service failed internally');
});
it('Should throw correct message when calling non existing local service', async function () {
it.skip('Should throw correct message when calling non existing local service', async function () {
// arrange
client = await createClient();
await peer.init();
// act
const res = callIdentifyOnInitPeerId(client);
const res = callIdentifyOnInitPeerId(peer);
// assert
await expect(res).rejects.toMatchObject({
@ -250,7 +251,7 @@ describe('Typescript usage suite', () => {
it('Should not crash if undefined is passed as a variable', async () => {
// arrange
client = await createClient();
await peer.init();
const [request, promise] = new RequestFlowBuilder()
.withRawScript(
`
@ -264,7 +265,7 @@ describe('Typescript usage suite', () => {
.buildAsFetch<any[]>('return', 'return');
// act
await client.initiateFlow(request);
await peer.internals.initiateFlow(request);
const [res] = await promise;
// assert
@ -273,14 +274,14 @@ describe('Typescript usage suite', () => {
it('Should throw correct error when the client tries to send a particle not to the relay', async () => {
// arrange
client = await createClient();
await peer.init();
// act
const [req, promise] = new RequestFlowBuilder()
.withRawScript('(call "incorrect_peer_id" ("any" "service") [])')
.buildWithErrorHandling();
await client.initiateFlow(req);
await peer.internals.initiateFlow(req);
// assert
await expect(promise).rejects.toMatch(
@ -289,7 +290,7 @@ describe('Typescript usage suite', () => {
});
});
async function callIdentifyOnInitPeerId(client: FluenceClient): Promise<string[]> {
async function callIdentifyOnInitPeerId(peer: FluencePeer): Promise<string[]> {
let request;
const promise = new Promise<string[]>((resolve, reject) => {
request = new RequestFlowBuilder()
@ -301,6 +302,6 @@ async function callIdentifyOnInitPeerId(client: FluenceClient): Promise<string[]
.handleScriptError(reject)
.build();
});
await client.initiateFlow(request);
await peer.internals.initiateFlow(request);
return promise;
}

View File

@ -1,12 +1,17 @@
import { CallServiceHandler, errorHandler, ResultCodes } from '../../internal/CallServiceHandler';
import { CallServiceData, CallServiceHandler, ResultCodes } from '../../internal/CallServiceHandler';
import { errorHandler } from '../../internal/defaultMiddlewares';
const req = () => ({
const req = (): CallServiceData => ({
serviceId: 'service',
fnName: 'fn name',
args: [],
tetraplets: [],
particleContext: {
particleId: 'id',
initPeerId: 'init peer id',
timestamp: 595951200,
ttl: 595961200,
signature: 'sig',
},
});
@ -102,7 +107,7 @@ describe('Call service handler tests', () => {
// assert
expect(res).toMatchObject({
retCode: ResultCodes.exceptionInHandler,
result: 'Error: some error',
result: 'Handler failed. fnName="fn name" serviceId="service" error: Error: some error',
});
});

View File

@ -0,0 +1,32 @@
import { encode } from 'bs58';
import * as base64 from 'base64-js';
import PeerId from 'peer-id';
import { KeyPair } from '../../internal/KeyPair';
describe('KeyPair tests', () => {
it('should create private key from seed and back', async function () {
// arrange
const sk = 'z1x3cVXhk9nJKE1pZaX9KxccUBzxu3aGlaUjDdAB2oY=';
// act
const keyPair = await KeyPair.fromEd25519SK(sk);
const sk2 = peerIdToEd25519SK(keyPair.Libp2pPeerId);
// assert
expect(sk2).toBe(sk);
});
});
/**
* Converts peer id into base64 string contatining the 32 byte Ed25519S secret key
* @returns - base64 of Ed25519S secret key
*/
export const peerIdToEd25519SK = (peerId: PeerId): string => {
// export as [...private, ...public] array
const privateAndPublicKeysArray = peerId.privKey.marshal();
// extract the private key
const pk = privateAndPublicKeysArray.slice(0, 32);
// serialize private key as base64
const b64 = base64.fromByteArray(pk);
return b64;
};

View File

@ -1,16 +1,16 @@
import { seedToPeerId } from '../../internal/peerIdUtils';
import { KeyPair } from '../../internal/KeyPair';
import { RequestFlow } from '../../internal/RequestFlow';
describe('Request flow tests', () => {
it('particle initiation should work', async () => {
// arrange
jest.useFakeTimers();
const seed = '4vzv3mg6cnjpEK24TXXLA3Ye7QrvKWPKqfbDvAKAyLK6';
const sk = 'z1x3cVXhk9nJKE1pZaX9KxccUBzxu3aGlaUjDdAB2oY=';
const mockDate = new Date(Date.UTC(2021, 2, 14)).valueOf();
Date.now = jest.fn(() => mockDate);
const request = RequestFlow.createLocal('(null)', 10000);
const peerId = await seedToPeerId(seed);
const peerId = await (await KeyPair.fromEd25519SK(sk)).Libp2pPeerId;
// act
await request.initState(peerId);

View File

@ -1,7 +1,7 @@
import { FluenceConnection } from '../../internal/FluenceConnection';
import Peer from 'libp2p';
import { Multiaddr } from 'multiaddr';
import { generatePeerId } from '../../internal/peerIdUtils';
import { KeyPair } from '../../internal/KeyPair';
describe('Ws Transport', () => {
// TODO:: fix test
@ -10,7 +10,7 @@ describe('Ws Transport', () => {
let multiaddr = new Multiaddr(
'/ip4/127.0.0.1/tcp/1234/ws/p2p/12D3KooWMJ78GJrtCxVUpjLEedbPtnLDxkFQJ2wuefEdrxq6zwSs',
);
let peerId = await generatePeerId();
let peerId = (await KeyPair.randomEd25519()).Libp2pPeerId;
const connection = new FluenceConnection(multiaddr, peerId, peerId, (_) => {});
await (connection as any).createPeer();
let node = (connection as any).node as Peer;

View File

@ -1,197 +0,0 @@
import { createClient, FluenceClient } from '../../FluenceClient';
import { RequestFlow } from '../../internal/RequestFlow';
import { RequestFlowBuilder } from '../../internal/RequestFlowBuilder';
let client: FluenceClient;
describe('== AIR suite', () => {
afterEach(async () => {
if (client) {
await client.disconnect();
}
});
it('check init_peer_id', async function () {
// arrange
const serviceId = 'test_service';
const fnName = 'return_first_arg';
const script = `(call %init_peer_id% ("${serviceId}" "${fnName}") [%init_peer_id%])`;
// prettier-ignore
const [request, promise] = new RequestFlowBuilder()
.withRawScript(script)
.buildAsFetch<string[]>(serviceId, fnName);
// act
client = await createClient();
await client.initiateFlow(request);
const [result] = await promise;
// assert
expect(result).toBe(client.selfPeerId);
});
it('call local function', async function () {
// arrange
const serviceId = 'test_service';
const fnName = 'return_first_arg';
client = await createClient();
let res;
client.callServiceHandler.on(serviceId, fnName, (args, _) => {
res = args[0];
return res;
});
// act
const arg = 'hello';
const script = `(call %init_peer_id% ("${serviceId}" "${fnName}") ["${arg}"])`;
await client.initiateFlow(RequestFlow.createLocal(script));
// assert
expect(res).toEqual(arg);
});
describe('error handling', () => {
it('call broken script', async function () {
// arrange
const script = `(incorrect)`;
// prettier-ignore
const [request, error] = new RequestFlowBuilder()
.withRawScript(script)
.buildWithErrorHandling();
// act
client = await createClient();
await client.initiateFlow(request);
// assert
await expect(error).rejects.toContain("air can't be parsed");
});
it('call script without ttl', async function () {
// arrange
const script = `(null)`;
// prettier-ignore
const [request, promise] = new RequestFlowBuilder()
.withTTL(1)
.withRawScript(script)
.buildAsFetch();
// act
client = await createClient();
await client.initiateFlow(request);
// assert
await expect(promise).rejects.toContain('Timed out after');
});
});
it('check particle arguments', async function () {
// arrange
const serviceId = 'test_service';
const fnName = 'return_first_arg';
const script = `(call %init_peer_id% ("${serviceId}" "${fnName}") [arg1])`;
// prettier-ignore
const [request, promise] = new RequestFlowBuilder()
.withRawScript(script)
.withVariable('arg1', 'hello')
.buildAsFetch<string[]>(serviceId, fnName);
// act
client = await createClient();
await client.initiateFlow(request);
const [result] = await promise;
// assert
expect(result).toEqual('hello');
});
it('check security tetraplet', async function () {
// arrange
const makeDataServiceId = 'make_data_service';
const makeDataFnName = 'make_data';
const getDataServiceId = 'get_data_service';
const getDataFnName = 'get_data';
client = await createClient();
client.callServiceHandler.on(makeDataServiceId, makeDataFnName, (args, _) => {
return {
field: 42,
};
});
let res;
client.callServiceHandler.on(getDataServiceId, getDataFnName, (args, tetraplets) => {
res = {
args: args,
tetraplets: tetraplets,
};
return args[0];
});
// act
const script = `
(seq
(call %init_peer_id% ("${makeDataServiceId}" "${makeDataFnName}") [] result)
(call %init_peer_id% ("${getDataServiceId}" "${getDataFnName}") [result.$.field])
)`;
await client.initiateFlow(new RequestFlowBuilder().withRawScript(script).build());
// assert
const tetraplet = res.tetraplets[0][0];
expect(tetraplet).toMatchObject({
service_id: 'make_data_service',
function_name: 'make_data',
json_path: '$.field',
});
});
it('check chain of services work properly', async function () {
// arrange
client = await createClient();
const serviceId1 = 'check1';
const fnName1 = 'fn1';
let res1;
client.callServiceHandler.on(serviceId1, fnName1, (args, _) => {
res1 = args[0];
return res1;
});
const serviceId2 = 'check2';
const fnName2 = 'fn2';
let res2;
client.callServiceHandler.on(serviceId2, fnName2, (args, _) => {
res2 = args[0];
return res2;
});
const serviceId3 = 'check3';
const fnName3 = 'fn3';
let res3;
client.callServiceHandler.on(serviceId3, fnName3, (args, _) => {
res3 = args;
return res3;
});
const arg1 = 'arg1';
const arg2 = 'arg2';
// act
const script = `(seq
(seq
(call %init_peer_id% ("${serviceId1}" "${fnName1}") ["${arg1}"] result1)
(call %init_peer_id% ("${serviceId2}" "${fnName2}") ["${arg2}"] result2))
(call %init_peer_id% ("${serviceId3}" "${fnName3}") [result1 result2]))
`;
await client.initiateFlow(new RequestFlowBuilder().withRawScript(script).build());
// assert
expect(res1).toEqual(arg1);
expect(res2).toEqual(arg2);
expect(res3).toEqual([res1, res2]);
});
});

View File

@ -1,44 +0,0 @@
import { AirInterpreter } from '@fluencelabs/avm';
describe('== AST parsing suite', () => {
it('parse simple script and return ast', async function () {
const interpreter = await AirInterpreter.create(
undefined as any,
undefined as any,
undefined as any,
undefined as any,
);
let ast = interpreter.parseAir(`
(call "node" ("service" "function") [1 2 3] output)
`);
ast = JSON.parse(ast);
expect(ast).toEqual({
Call: {
peer_part: { PeerPk: { Literal: 'node' } },
function_part: { ServiceIdWithFuncName: [{ Literal: 'service' }, { Literal: 'function' }] },
args: [
{
Number: {
Int: 1,
},
},
{
Number: {
Int: 2,
},
},
{
Number: {
Int: 3,
},
},
],
output: {
Variable: { Scalar: 'output' },
},
},
});
});
});

View File

@ -43,6 +43,10 @@ describe('Tests for default handler', () => {
tetraplets: [],
particleContext: {
particleId: 'some',
initPeerId: 'init peer id',
timestamp: 595951200,
ttl: 595961200,
signature: 'sig',
},
};

View File

@ -1,13 +0,0 @@
import { encode } from 'bs58';
import { peerIdToSeed, seedToPeerId } from '../..';
describe('Peer Id utils', () => {
it('should create private key from seed and back', async function () {
// prettier-ignore
let seed = [46, 188, 245, 171, 145, 73, 40, 24, 52, 233, 215, 163, 54, 26, 31, 221, 159, 179, 126, 106, 27, 199, 189, 194, 80, 133, 235, 42, 42, 247, 80, 201];
let seedStr = encode(seed);
let pid = await seedToPeerId(seedStr);
expect(peerIdToSeed(pid)).toEqual(seedStr);
});
});

View File

@ -1,159 +0,0 @@
import { RequestFlowBuilder } from './internal/RequestFlowBuilder';
import { FluenceClient } from './FluenceClient';
import { CallServiceResultType } from './internal/CallServiceHandler';
import { SecurityTetraplet } from '@fluencelabs/avm';
/**
* The class representing Particle - a data structure used to perform operations on Fluence Network. It originates on some peer in the network, travels the network through a predefined path, triggering function execution along its way.
*/
export class Particle {
script: string;
data: Map<string, any>;
ttl: number;
/**
* Creates a particle with specified parameters.
* @param { String }script - Air script which defines the execution of a particle its path, functions it triggers on peers, and so on.
* @param { Map<string, any> | Record<string, any> } data - Variables passed to the particle in the form of either JS Map or JS object with keys representing variable names and values representing values correspondingly
* @param { [Number]=7000 } ttl - Time to live, a timout after which the particle execution is stopped by AVM.
*/
constructor(script: string, data?: Map<string, any> | Record<string, any>, ttl?: number) {
this.script = script;
if (data === undefined) {
this.data = new Map();
} else if (data instanceof Map) {
this.data = data;
} else {
this.data = new Map();
for (let k in data) {
this.data.set(k, data[k]);
}
}
this.ttl = ttl ?? 7000;
}
}
/**
* Send a particle to Fluence Network using the specified Fluence Client.
* @param { FluenceClient } client - The Fluence Client instance.
* @param { Particle } particle - The particle to send.
*/
export const sendParticle = async (
client: FluenceClient,
particle: Particle,
onError?: (err) => void,
): Promise<string> => {
const [req, errorPromise] = new RequestFlowBuilder()
.withRawScript(particle.script)
.withVariables(particle.data)
.withTTL(particle.ttl)
.buildWithErrorHandling();
errorPromise.catch(onError);
await client.initiateFlow(req);
return req.id;
};
/*
This map stores functions which unregister callbacks registered by registerServiceFunction
The key sould be created with makeKey. The value is the unresitration function
This is only needed to support legacy api
*/
const handlersUnregistratorsMap = new Map();
const makeKey = (client: FluenceClient, serviceId: string, fnName: string) => {
const pid = client.selfPeerId || '';
return `${pid}/${serviceId}/${fnName}`;
};
/**
* Registers a function which can be called on the client from AVM. The registration is per client basis.
* @param { FluenceClient } client - The Fluence Client instance.
* @param { string } serviceId - The identifier of service which would be used to make calls from AVM
* @param { string } fnName - The identifier of function which would be used to make calls from AVM
* @param { (args: any[], tetraplets: SecurityTetraplet[][]) => object | boolean | number | string } handler - The handler which would be called by AVM. The result is any object passed back to AVM
*/
export const registerServiceFunction = (
client: FluenceClient,
serviceId: string,
fnName: string,
handler: (args: any[], tetraplets: SecurityTetraplet[][]) => CallServiceResultType,
) => {
const unregister = client.callServiceHandler.on(serviceId, fnName, handler);
handlersUnregistratorsMap.set(makeKey(client, serviceId, fnName), unregister);
};
// prettier-ignore
/**
* Removes registers for the function previously registered with {@link registerServiceFunction}
* @param { FluenceClient } client - The Fluence Client instance.
* @param { string } serviceId - The identifier of service used in {@link registerServiceFunction} call
* @param { string } fnName - The identifier of function used in {@link registerServiceFunction} call
*/
export const unregisterServiceFunction = (
client: FluenceClient,
serviceId: string,
fnName: string
) => {
const key = makeKey(client, serviceId, fnName);
const unuse = handlersUnregistratorsMap.get(key);
if(unuse) {
unuse();
}
handlersUnregistratorsMap.delete(key);
};
/**
* Registers an event-like handler for all calls to the specific service\function pair from AVM. The registration is per client basis. Return a function which when called removes the subscription.
* Same as registerServiceFunction which immediately returns empty object.
* @param { FluenceClient } client - The Fluence Client instance.
* @param { string } serviceId - The identifier of service calls to which from AVM are transformed into events.
* @param { string } fnName - The identifier of function calls to which from AVM are transformed into events.
* @param { (args: any[], tetraplets: SecurityTetraplet[][]) => object } handler - The handler which would be called by AVM
* @returns { Function } - A function which when called removes the subscription.
*/
export const subscribeToEvent = (
client: FluenceClient,
serviceId: string,
fnName: string,
handler: (args: any[], tetraplets: SecurityTetraplet[][]) => void,
): Function => {
const realHandler = (args: any[], tetraplets: SecurityTetraplet[][]) => {
// dont' block
setTimeout(() => {
handler(args, tetraplets);
}, 0);
return {};
};
registerServiceFunction(client, serviceId, fnName, realHandler);
return () => {
unregisterServiceFunction(client, serviceId, fnName);
};
};
/**
* Send a particle with a fetch-like semantics. In order to for this to work you have to you have to make a call to the same callbackServiceId\callbackFnName pair from Air script as specified by the parameters. The arguments of the call are returned as the resolve value of promise
* @param { FluenceClient } client - The Fluence Client instance.
* @param { Particle } particle - The particle to send.
* @param { string } callbackFnName - The identifier of function which should be used in Air script to pass the data to fetch "promise"
* @param { [string]='_callback' } callbackServiceId - The service identifier which should be used in Air script to pass the data to fetch "promise"
* @returns { Promise<T> } - A promise which would be resolved with the data returned from AVM
*/
export const sendParticleAsFetch = async <T>(
client: FluenceClient,
particle: Particle,
callbackFnName: string,
callbackServiceId: string = '_callback',
): Promise<T> => {
const [request, promise] = new RequestFlowBuilder()
.withRawScript(particle.script)
.withVariables(particle.data)
.withTTL(particle.ttl)
.buildAsFetch<T>(callbackServiceId, callbackFnName);
await client.initiateFlow(request);
return promise;
};

View File

@ -1,2 +0,0 @@
export { RequestFlowBuilder } from './internal/RequestFlowBuilder';
export * from './internal/CallServiceHandler';

View File

@ -14,14 +14,12 @@
* limitations under the License.
*/
export { seedToPeerId, peerIdToSeed, generatePeerId } from './internal/peerIdUtils';
export { PeerIdB58 } from './internal/commonTypes';
export { SecurityTetraplet } from '@fluencelabs/avm';
export * from './api';
export * from './FluenceClient';
export * from './internal/builtins';
import log, { LogLevelDesc } from 'loglevel';
export { KeyPair } from './internal/KeyPair';
export { FluencePeer, AvmLoglevel } from './internal/FluencePeer';
export { PeerIdB58, CallParams } from './internal/commonTypes';
export const setLogLevel = (level: LogLevelDesc) => {
log.setLevel(level);
};

View File

@ -1,10 +1,10 @@
import { SecurityTetraplet } from '@fluencelabs/avm';
import { PeerIdB58 } from './commonTypes';
export enum ResultCodes {
success = 0,
noServiceFound = 1,
unkownError = 1,
exceptionInHandler = 2,
unkownError = 1024,
}
/**
@ -15,7 +15,10 @@ interface ParticleContext {
* The particle ID
*/
particleId: string;
[x: string]: any;
initPeerId: PeerIdB58;
timestamp: number;
ttl: number;
signature: string;
}
/**
@ -82,6 +85,20 @@ export interface CallServiceResult {
*/
export type Middleware = (req: CallServiceData, resp: CallServiceResult, next: Function) => void;
export class CallServiceArg<T> {
val: T;
tetraplet: SecurityTetraplet[];
constructor(val: T, tetraplet: SecurityTetraplet[]) {
this.val = val;
this.tetraplet = tetraplet;
}
}
type CallParams = ParticleContext & {
wrappedArgs: CallServiceArg<any>[];
};
/**
* Convenience middleware factory. Registeres a handler for a pair of 'serviceId/fnName'.
* The return value of the handler is passed back to AVM
@ -92,11 +109,11 @@ export type Middleware = (req: CallServiceData, resp: CallServiceResult, next: F
export const fnHandler = (
serviceId: string,
fnName: string,
handler: (args: any[], tetraplets: SecurityTetraplet[][]) => CallServiceResultType,
handler: (args: any[], callParams: CallParams) => CallServiceResultType,
) => {
return (req: CallServiceData, resp: CallServiceResult, next: Function): void => {
if (req.fnName === fnName && req.serviceId === serviceId) {
const res = handler(req.args, req.tetraplets);
const res = handler(req.args, { ...req.particleContext, wrappedArgs: req.wrappedArgs });
resp.retCode = ResultCodes.success;
resp.result = res;
}
@ -112,14 +129,14 @@ export const fnHandler = (
* @param { (args: any[], tetraplets: SecurityTetraplet[][]) => void } handler - The handler which should handle the call.
*/
export const fnAsEventHandler = (
serviceId: string,
serviceId: string, // force format
fnName: string,
handler: (args: any[], tetraplets: SecurityTetraplet[][]) => void,
handler: (args: any[], callParams: CallParams) => void,
) => {
return (req: CallServiceData, resp: CallServiceResult, next: Function): void => {
if (req.fnName === fnName && req.serviceId === serviceId) {
setTimeout(() => {
handler(req.args, req.tetraplets);
handler(req.args, { ...req.particleContext, wrappedArgs: req.wrappedArgs });
}, 0);
resp.retCode = ResultCodes.success;
@ -129,18 +146,6 @@ export const fnAsEventHandler = (
};
};
/**
* Error catching middleware
*/
export const errorHandler: Middleware = (req: CallServiceData, resp: CallServiceResult, next: Function): void => {
try {
next();
} catch (e) {
resp.retCode = ResultCodes.exceptionInHandler;
resp.result = e.toString();
}
};
type CallServiceFunction = (req: CallServiceData, resp: CallServiceResult) => void;
/**
@ -188,9 +193,9 @@ export class CallServiceHandler {
* Convinience method for registring @see { @link fnHandler } middleware
*/
on(
serviceId: string,
serviceId: string, // force format
fnName: string,
handler: (args: any[], tetraplets: SecurityTetraplet[][]) => CallServiceResultType,
handler: (args: any[], callParams: CallParams) => CallServiceResultType,
): Function {
const mw = fnHandler(serviceId, fnName, handler);
this.use(mw);
@ -203,9 +208,9 @@ export class CallServiceHandler {
* Convinience method for registring @see { @link fnAsEventHandler } middleware
*/
onEvent(
serviceId: string,
serviceId: string, // force format
fnName: string,
handler: (args: any[], tetraplets: SecurityTetraplet[][]) => void,
handler: (args: any[], callParams: CallParams) => void,
): Function {
const mw = fnAsEventHandler(serviceId, fnName, handler);
this.use(mw);

View File

@ -1,236 +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 * as PeerId from 'peer-id';
import { Multiaddr } from 'multiaddr';
import { FluenceConnection, FluenceConnectionOptions } from './FluenceConnection';
import { PeerIdB58 } from './commonTypes';
import { FluenceClient } from '../FluenceClient';
import { RequestFlow } from './RequestFlow';
import { CallServiceHandler } from './CallServiceHandler';
import { loadRelayFn, loadVariablesService } from './RequestFlowBuilder';
import { logParticle, Particle } from './particle';
import log from 'loglevel';
import {
AirInterpreter,
ParticleHandler,
SecurityTetraplet,
CallServiceResult,
LogLevel as AvmLogLevel,
} from '@fluencelabs/avm';
import makeDefaultClientHandler from './defaultClientHandler';
const createClient = (handler, peerId): Promise<AirInterpreter> => {
let logLevel: AvmLogLevel = 'off';
switch (log.getLevel()) {
case 0: // 'TRACE'
logLevel = 'trace';
break;
case 1: // 'DEBUG'
logLevel = 'debug';
break;
case 2: // 'INFO'
logLevel = 'info';
break;
case 3: // 'WARN'
logLevel = 'warn';
break;
case 4: // 'ERROR'
logLevel = 'error';
break;
case 5: // 'SILENT'
logLevel = 'off';
break;
}
const logFn = (level: AvmLogLevel, msg: string) => {
switch (level) {
case 'error':
log.error(msg);
break;
case 'warn':
log.warn(msg);
break;
case 'info':
log.info(msg);
break;
case 'debug':
case 'trace':
log.log(msg);
break;
}
};
return AirInterpreter.create(handler, peerId, logLevel, logFn);
};
export class ClientImpl implements FluenceClient {
readonly selfPeerIdFull: PeerId;
private requests: Map<string, RequestFlow> = new Map();
private currentRequestId: string | null = null;
private watchDog;
get relayPeerId(): PeerIdB58 | undefined {
return this.connection?.nodePeerId.toB58String();
}
get selfPeerId(): PeerIdB58 {
return this.selfPeerIdFull.toB58String();
}
get isConnected(): boolean {
return this.connection?.isConnected();
}
private connection: FluenceConnection;
private interpreter: AirInterpreter;
constructor(selfPeerIdFull: PeerId) {
this.selfPeerIdFull = selfPeerIdFull;
this.callServiceHandler = makeDefaultClientHandler();
}
callServiceHandler: CallServiceHandler;
async disconnect(): Promise<void> {
if (this.connection) {
await this.connection.disconnect();
}
this.clearWathcDog();
this.requests.forEach((r) => {
r.cancel();
});
}
async initAirInterpreter(): Promise<void> {
this.interpreter = await createClient(this.interpreterCallback.bind(this), this.selfPeerId);
}
async connect(multiaddr: string | Multiaddr, options?: FluenceConnectionOptions): Promise<void> {
multiaddr = new Multiaddr(multiaddr);
const nodePeerId = multiaddr.getPeerId();
if (!nodePeerId) {
throw Error("'multiaddr' did not contain a valid peer id");
}
if (this.connection) {
await this.connection.disconnect();
}
const node = PeerId.createFromB58String(nodePeerId);
const connection = new FluenceConnection(
multiaddr,
node,
this.selfPeerIdFull,
this.executeIncomingParticle.bind(this),
);
await connection.connect(options);
this.connection = connection;
this.initWatchDog();
}
async initiateFlow(request: RequestFlow): Promise<void> {
// setting `relayVariableName` here. If the client is not connected (i.e it is created as local) then there is no relay
request.handler.on(loadVariablesService, loadRelayFn, () => {
return this.relayPeerId || '';
});
await request.initState(this.selfPeerIdFull);
logParticle(log.debug, 'executing local particle', request.getParticle());
request.handler.combineWith(this.callServiceHandler);
this.requests.set(request.id, request);
this.processRequest(request);
}
async executeIncomingParticle(particle: Particle) {
logParticle(log.debug, 'external particle received', particle);
let request = this.requests.get(particle.id);
if (request) {
request.receiveUpdate(particle);
} else {
request = RequestFlow.createExternal(particle);
request.handler.combineWith(this.callServiceHandler);
}
this.requests.set(request.id, request);
await this.processRequest(request);
}
private processRequest(request: RequestFlow) {
try {
this.currentRequestId = request.id;
request.execute(this.interpreter, this.connection, this.relayPeerId);
} catch (err) {
log.error('particle processing failed: ' + err);
} finally {
this.currentRequestId = null;
}
}
private interpreterCallback: ParticleHandler = (
serviceId: string,
fnName: string,
args: any[],
tetraplets: SecurityTetraplet[][],
): CallServiceResult => {
if (this.currentRequestId === null) {
throw Error('current request can`t be null here');
}
const request = this.requests.get(this.currentRequestId);
const res = request.handler.execute({
serviceId,
fnName,
args,
tetraplets,
particleContext: {
particleId: request.id,
},
});
if (res.result === undefined) {
log.error(
`Call to serviceId=${serviceId} fnName=${fnName} unexpectedly returned undefined result, falling back to null`,
);
res.result = null;
}
return {
ret_code: res.retCode,
result: JSON.stringify(res.result),
};
};
private initWatchDog() {
this.watchDog = setInterval(() => {
for (let key in this.requests.keys) {
if (this.requests.get(key).hasExpired()) {
this.requests.delete(key);
}
}
}, 5000);
}
private clearWathcDog() {
clearInterval(this.watchDog);
}
}

View File

@ -118,7 +118,8 @@ export class FluenceConnection {
try {
await this.node.dial(this.address);
} catch (e) {
} catch (e1) {
const e = e1 as any;
if (e.name === 'AggregateError' && e._errors.length === 1) {
const error = e._errors[0];
throw `Error dialing node ${this.address}:\n${error.code}\n${error.message}`;

325
src/internal/FluencePeer.ts Normal file
View File

@ -0,0 +1,325 @@
import { AirInterpreter, CallServiceResult, LogLevel, ParticleHandler, SecurityTetraplet } from '@fluencelabs/avm';
import log from 'loglevel';
import { Multiaddr } from 'multiaddr';
import PeerId from 'peer-id';
import { CallServiceHandler } from './CallServiceHandler';
import { PeerIdB58 } from './commonTypes';
import makeDefaultClientHandler from './defaultClientHandler';
import { FluenceConnection, FluenceConnectionOptions } from './FluenceConnection';
import { logParticle, Particle } from './particle';
import { KeyPair } from './KeyPair';
import { RequestFlow } from './RequestFlow';
import { loadRelayFn, loadVariablesService } from './RequestFlowBuilder';
import { createInterpreter } from './utils';
/**
* Node of the Fluence detwork specified as a pair of node's multiaddr and it's peer id
*/
type Node = {
peerId: PeerIdB58;
multiaddr: string;
};
/**
* Enum representing the log level used in Aqua VM.
* Possible values: 'info', 'trace', 'debug', 'info', 'warn', 'error', 'off';
*/
export type AvmLoglevel = LogLevel;
/**
* Configuration used when initiating Fluence Peer
*/
export interface PeerConfig {
/**
* Node in Fluence network to 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
* If not specified the will work locally and would not be able to send or receive particles.
*/
connectTo?: string | Multiaddr | Node;
avmLogLevel?: AvmLoglevel;
/**
* Specify the KeyPair to be used to identify the Fluence Peer.
* Will be generated randomly if not specified
*/
KeyPair?: KeyPair;
/**
* When the peer established the connection to the network it sends a ping-like message to check if it works correctly.
* The options allows to specify the timeout for that message in milliseconds.
* If not specified the default timeout will be used
*/
checkConnectionTimeoutMs?: number;
/**
* When the peer established the connection to the network it sends a ping-like message to check if it works correctly.
* If set to true, the ping-like message will be skipped
* Default: false
*/
skipCheckConnection?: boolean;
/**
* The dialing timeout in milliseconds
*/
dialTimeoutMs?: number;
}
/**
* Information about Fluence Peer connection
*/
interface ConnectionInfo {
/**
* Is the peer connected to network or not
*/
isConnected: Boolean;
/**
* The Peer's identification in the Fluence network
*/
selfPeerId: PeerIdB58;
/**
* The relays's peer id to which the peer is connected to
*/
connectedRelay: PeerIdB58 | 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 {
/**
* Creates a new Fluence Peer instance. Does not start the workflows.
* In order to work with the Peer it has to be initialized with the `init` method
*/
constructor() {}
/**
* Get the information about Fluence Peer connections
*/
get connectionInfo(): ConnectionInfo {
const isConnected = this._connection?.isConnected();
return {
isConnected: isConnected,
selfPeerId: this._selfPeerId,
connectedRelay: this._relayPeerId || null,
};
}
/**
* 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 init(config?: PeerConfig): Promise<void> {
if (config?.KeyPair) {
this._keyPair = config!.KeyPair;
} else {
this._keyPair = await KeyPair.randomEd25519();
}
await this._initAirInterpreter(config?.avmLogLevel || 'off');
this._callServiceHandler = makeDefaultClientHandler();
if (config?.connectTo) {
let theAddress: Multiaddr;
let fromNode = (config.connectTo as any).multiaddr;
if (fromNode) {
theAddress = new Multiaddr(fromNode);
} else {
theAddress = new Multiaddr(config.connectTo as string);
}
await this._connect(theAddress);
}
}
/**
* Uninitializes the peer: stops all the underltying workflows, stops the Aqua VM
* and disconnects from the Fluence network
*/
async uninit() {
await this._disconnect();
this._callServiceHandler = null;
}
/**
* Get the default Fluence peer instance. The default peer is used automatically in all the functions generated
* by the Aqua compiler if not specified otherwise.
*/
static get default(): FluencePeer {
return this._default;
}
// internal api
/**
* Does not intended to be used manually. Subject to change
*/
get internals() {
return {
initiateFlow: this._initiateFlow.bind(this),
callServiceHandler: this._callServiceHandler,
};
}
// private
private async _initiateFlow(request: RequestFlow): Promise<void> {
// setting `relayVariableName` here. If the client is not connected (i.e it is created as local) then there is no relay
request.handler.on(loadVariablesService, loadRelayFn, () => {
return this._relayPeerId || '';
});
await request.initState(this._keyPair.Libp2pPeerId);
logParticle(log.debug, 'executing local particle', request.getParticle());
request.handler.combineWith(this._callServiceHandler);
this._requests.set(request.id, request);
this._processRequest(request);
}
private _callServiceHandler: CallServiceHandler;
private static _default: FluencePeer = new FluencePeer();
private _keyPair: KeyPair;
private _requests: Map<string, RequestFlow> = new Map();
private _currentRequestId: string | null = null;
private _watchdog;
private _connection: FluenceConnection;
private _interpreter: AirInterpreter;
private async _initAirInterpreter(logLevel: AvmLoglevel): Promise<void> {
this._interpreter = await createInterpreter(this._interpreterCallback.bind(this), this._selfPeerId, logLevel);
}
private async _connect(multiaddr: Multiaddr, options?: FluenceConnectionOptions): Promise<void> {
const nodePeerId = multiaddr.getPeerId();
if (!nodePeerId) {
throw Error("'multiaddr' did not contain a valid peer id");
}
if (this._connection) {
await this._connection.disconnect();
}
const node = PeerId.createFromB58String(nodePeerId);
const connection = new FluenceConnection(
multiaddr,
node,
this._keyPair.Libp2pPeerId,
this._executeIncomingParticle.bind(this),
);
await connection.connect(options);
this._connection = connection;
this._initWatchDog();
}
private async _disconnect(): Promise<void> {
if (this._connection) {
await this._connection.disconnect();
}
this._clearWathcDog();
this._requests.forEach((r) => {
r.cancel();
});
}
private get _selfPeerId(): PeerIdB58 {
return this._keyPair.Libp2pPeerId.toB58String();
}
private get _relayPeerId(): PeerIdB58 | undefined {
return this._connection?.nodePeerId.toB58String();
}
private async _executeIncomingParticle(particle: Particle) {
logParticle(log.debug, 'incoming particle received', particle);
let request = this._requests.get(particle.id);
if (request) {
await request.receiveUpdate(particle);
} else {
request = RequestFlow.createExternal(particle);
request.handler.combineWith(this._callServiceHandler);
}
this._requests.set(request.id, request);
await this._processRequest(request);
}
private _processRequest(request: RequestFlow) {
try {
this._currentRequestId = request.id;
request.execute(this._interpreter, this._connection, this._relayPeerId);
} catch (err) {
log.error('particle processing failed: ' + err);
} finally {
this._currentRequestId = null;
}
}
private _interpreterCallback: ParticleHandler = (
serviceId: string,
fnName: string,
args: any[],
tetraplets: SecurityTetraplet[][],
): CallServiceResult => {
if (this._currentRequestId === null) {
throw Error('current request can`t be null here');
}
const request = this._requests.get(this._currentRequestId);
const particle = request.getParticle();
if (particle === null) {
throw new Error("particle can't be null here, current request id: " + this._currentRequestId);
}
const res = request.handler.execute({
serviceId,
fnName,
args,
tetraplets,
particleContext: {
particleId: request.id,
initPeerId: particle.init_peer_id,
timestamp: particle.timestamp,
ttl: particle.ttl,
signature: particle.signature,
},
});
if (res.result === undefined) {
log.error(
`Call to serviceId=${serviceId} fnName=${fnName} unexpectedly returned undefined result, falling back to null. Particle id=${request.id}`,
);
res.result = null;
}
return {
ret_code: res.retCode,
result: JSON.stringify(res.result),
};
};
private _initWatchDog() {
this._watchdog = setInterval(() => {
for (let key in this._requests.keys) {
if (this._requests.get(key).hasExpired()) {
this._requests.delete(key);
}
}
}, 5000); // TODO: make configurable
}
private _clearWathcDog() {
clearInterval(this._watchdog);
}
}

60
src/internal/KeyPair.ts Normal file
View File

@ -0,0 +1,60 @@
/*
* 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 * as PeerId from 'peer-id';
import * as base64 from 'base64-js';
import * as ed from 'noble-ed25519';
import { keys } from 'libp2p-crypto';
export class KeyPair {
/**
* @deprecated
* Key pair in libp2p format. Used for backward compatibility with the current FluencePeer implementation
*/
public Libp2pPeerId: PeerId;
/**
* Generates a new KeyPair from base64 string contatining the 32 byte Ed25519 secret key
* @returns - Promise with the created KeyPair
*/
static async fromEd25519SK(sk: string): Promise<KeyPair> {
// deserialize secret key from base64
const bytes = base64.toByteArray(sk);
// calculate ed25519 public key
const publicKey = await ed.getPublicKey(bytes);
// concatenate secret + public because that's what libp2p-crypto expects
const privateAndPublicKeysArray = new Uint8Array([...bytes, ...publicKey]);
// deserialize keys.supportedKeys.Ed25519PrivateKey
const privateKey = await keys.supportedKeys.ed25519.unmarshalEd25519PrivateKey(privateAndPublicKeysArray);
// serialize it to protobuf encoding because that's what PeerId expects
const protobuf = keys.marshalPrivateKey(privateKey);
// deserialize PeerId from protobuf encoding
const lib2p2Pid = await PeerId.createFromPrivKey(protobuf);
const res = new KeyPair();
res.Libp2pPeerId = lib2p2Pid;
return res;
}
/**
* Generates a new KeyPair with random secret key
* @returns - Promise with the created KeyPair
*/
static async randomEd25519(): Promise<KeyPair> {
const res = new KeyPair();
res.Libp2pPeerId = await PeerId.create({ keyType: 'Ed25519' });
return res;
}
}

View File

@ -5,6 +5,7 @@ import { CallServiceHandler } from './CallServiceHandler';
import { PeerIdB58 } from './commonTypes';
import { FluenceConnection } from './FluenceConnection';
import { Particle, genUUID, logParticle } from './particle';
import { ParticleDataToString } from './utils';
export const DEFAULT_TTL = 7000;

View File

@ -1,342 +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 { FluenceClient } from '../FluenceClient';
import { ModuleConfig } from './moduleConfig';
import { RequestFlowBuilder } from './RequestFlowBuilder';
const nodeIdentityCall = (client: FluenceClient): string => {
return `(call "${client.relayPeerId}" ("op" "identity") [])`;
};
const requestResponse = async <T>(
client: FluenceClient,
name: string,
call: (nodeId: string) => string,
returnValue: string,
data: Map<string, any>,
handleResponse: (args: any[]) => T,
nodeId?: string,
ttl?: number,
): Promise<T> => {
if (!ttl) {
ttl = 10000;
}
if (!nodeId) {
nodeId = client.relayPeerId;
}
let serviceCall = call(nodeId);
let script = `(seq
${nodeIdentityCall(client)}
(seq
(seq
${serviceCall}
${nodeIdentityCall(client)}
)
(call "${client.selfPeerId}" ("_callback" "${name}") [${returnValue}])
)
)
`;
const [request, promise] = new RequestFlowBuilder()
.withRawScript(script)
.withVariables(data)
.withTTL(ttl)
.buildAsFetch<any[]>('_callback', name);
await client.initiateFlow(request);
const res = await promise;
return handleResponse(res);
};
/**
* Get all available modules hosted on a connected relay. @deprecated prefer using raw Particles instead
* @param { FluenceClient } client - The Fluence Client instance.
* @returns { Array<string> } - list of available modules on the connected relay
*/
export const getModules = async (client: FluenceClient, ttl?: number): Promise<string[]> => {
let callbackFn = 'getModules';
const [req, promise] = new RequestFlowBuilder()
.withRawScript(
`
(seq
(call __relay ("dist" "list_modules") [] result)
(call myPeerId ("_callback" "${callbackFn}") [result])
)
`,
)
.withVariables({
__relay: client.relayPeerId,
myPeerId: client.selfPeerId,
})
.withTTL(ttl)
.buildAsFetch<[string[]]>('_callback', callbackFn);
client.initiateFlow(req);
const [res] = await promise;
return res;
};
/**
* Get all available interfaces hosted on a connected relay. @deprecated prefer using raw Particles instead
* @param { FluenceClient } client - The Fluence Client instance.
* @returns { Array<string> } - list of available interfaces on the connected relay
*/
export const getInterfaces = async (client: FluenceClient, ttl?: number): Promise<string[]> => {
let callbackFn = 'getInterfaces';
const [req, promise] = new RequestFlowBuilder()
.withRawScript(
`
(seq
(seq
(seq
(call relay ("srv" "list") [] services)
(call relay ("op" "identity") [] $interfaces)
)
(fold services s
(seq
(call relay ("srv" "get_interface") [s.$.id!] $interfaces)
(next s)
)
)
)
(call myPeerId ("_callback" "${callbackFn}") [$interfaces])
)
`,
)
.withVariables({
relay: client.relayPeerId,
myPeerId: client.selfPeerId,
})
.withTTL(ttl)
.buildAsFetch<[string[]]>('_callback', callbackFn);
client.initiateFlow(req);
const [res] = await promise;
return res;
};
/**
* Send a script to add module to a relay. Waiting for a response from a relay. @deprecated prefer using raw Particles instead
* @param { FluenceClient } client - The Fluence Client instance.
* @param { string } name - Name of the uploaded module
* @param { string } moduleBase64 - Base64 content of the module
* @param { ModuleConfig } config - Module config
*/
export const uploadModule = async (
client: FluenceClient,
name: string,
moduleBase64: string,
config?: ModuleConfig,
ttl?: number,
): Promise<void> => {
if (!config) {
config = {
name: name,
mem_pages_count: 100,
logger_enabled: true,
wasi: {
envs: {},
preopened_files: ['/tmp'],
mapped_dirs: {},
},
};
}
let data = new Map();
data.set('module_bytes', moduleBase64);
data.set('module_config', config);
data.set('__relay', client.relayPeerId);
data.set('myPeerId', client.selfPeerId);
const [req, promise] = new RequestFlowBuilder()
.withRawScript(
`
(seq
(call __relay ("dist" "add_module") [module_bytes module_config] result)
(call myPeerId ("_callback" "getModules") [result])
)
`,
)
.withVariables(data)
.withTTL(ttl)
.buildAsFetch<[string[]]>('_callback', 'getModules');
await client.initiateFlow(req);
await promise;
};
/**
* Send a script to add module to a relay. Waiting for a response from a relay. @deprecated prefer using raw Particles instead
* @param { FluenceClient } client - The Fluence Client instance.
* @param { string } name - Name of the blueprint
* @param { Array<string> } dependencies - Array of it's dependencies
* @param {[string]} blueprintId - Optional blueprint ID
* @param {[string]} nodeId - Optional node peer id to deploy blueprint to
* @param {[number]} ttl - Optional ttl for the particle which does the job
* @returns { string } - Created blueprint ID
*/
export const addBlueprint = async (
client: FluenceClient,
name: string,
dependencies: string[],
blueprintId?: string,
nodeId?: string,
ttl?: number,
): Promise<string> => {
let returnValue = 'blueprint_id';
let call = (nodeId: string) => `(call "${nodeId}" ("dist" "add_blueprint") [blueprint] ${returnValue})`;
let data = new Map();
data.set('blueprint', { name: name, dependencies: dependencies, id: blueprintId });
return requestResponse(
client,
'addBlueprint',
call,
returnValue,
data,
(args: any[]) => args[0] as string,
nodeId,
ttl,
);
};
/**
* Send a script to create a service on the connected relay. Waiting for a response from the relay. @deprecated prefer using raw Particles instead
* @param { FluenceClient } client - The Fluence Client instance.
* @param {string} blueprintId - The blueprint of the service
* @param {[string]} nodeId - Optional node peer id to deploy service to
* @param {[number]} ttl - Optional ttl for the particle which does the job
* @returns { string } - Created service ID
*/
export const createService = async (
client: FluenceClient,
blueprintId: string,
nodeId?: string,
ttl?: number,
): Promise<string> => {
let returnValue = 'service_id';
let call = (nodeId: string) => `(call "${nodeId}" ("srv" "create") [blueprint_id] ${returnValue})`;
let data = new Map();
data.set('blueprint_id', blueprintId);
return requestResponse(
client,
'createService',
call,
returnValue,
data,
(args: any[]) => args[0] as string,
nodeId,
ttl,
);
};
/**
* Get all available blueprints hosted on a connected relay. @deprecated prefer using raw Particles instead
* @param { FluenceClient } client - The Fluence Client instance.
* @param {[string]} nodeId - Optional node peer id to get available blueprints from
* @param {[string]} nodeId - Optional node peer id to deploy service to
* @param {[number]} ttl - Optional ttl for the particle which does the job
* @returns { Array<object> } - List of available blueprints
*/
export const getBlueprints = async (
client: FluenceClient,
nodeId?: string,
ttl?: number,
): Promise<[{ dependencies; id: string; name: string }]> => {
let returnValue = 'blueprints';
let call = (nodeId: string) => `(call "${nodeId}" ("dist" "list_blueprints") [] ${returnValue})`;
return requestResponse(
client,
'getBlueprints',
call,
returnValue,
new Map(),
(args: any[]) => args[0],
nodeId,
ttl,
);
};
/**
* Get relays neighborhood. @deprecated prefer using raw Particles instead
* @param { FluenceClient } client - The Fluence Client instance.
* @param {[string]} nodeId - Optional node peer id to get neighborhood from
* @param {[number]} ttl - Optional ttl for the particle which does the job
* @returns { Array<string> } - List of peer ids of neighbors of the node
*/
export const neighborhood = async (client: FluenceClient, nodeId?: string, ttl?: number): Promise<string[]> => {
let returnValue = 'neighborhood';
let call = (nodeId: string) => `(call "${nodeId}" ("dht" "neighborhood") [node] ${returnValue})`;
let data = new Map();
if (nodeId) data.set('node', nodeId);
return requestResponse(client, 'neighborhood', call, returnValue, data, (args) => args[0] as string[], nodeId, ttl);
};
/**
* Upload an AIR script, that will be runned in a loop on a node. @deprecated prefer using raw Particles instead
* @param { FluenceClient } client - The Fluence Client instance.
* @param {[string]} script - script to upload
* @param period how often start script processing, in seconds
* @param {[string]} nodeId - Optional node peer id to get neighborhood from
* @param {[number]} ttl - Optional ttl for the particle which does the job
* @returns {[string]} - script id
*/
export const addScript = async (
client: FluenceClient,
script: string,
period?: number,
nodeId?: string,
ttl?: number,
): Promise<string> => {
let returnValue = 'id';
let periodV = '';
if (period) periodV = period.toString();
let call = (nodeId: string) => `(call "${nodeId}" ("script" "add") [script ${periodV}] ${returnValue})`;
let data = new Map();
data.set('script', script);
if (period) data.set('period', period);
return requestResponse(client, 'addScript', call, returnValue, data, (args) => args[0] as string, nodeId, ttl);
};
/**
* Remove an AIR script from a node. @deprecated prefer using raw Particles instead
* @param { FluenceClient } client - The Fluence Client instance.
* @param {[string]} id - id of a script
* @param {[string]} nodeId - Optional node peer id to get neighborhood from
* @param {[number]} ttl - Optional ttl for the particle which does the job
*/
export const removeScript = async (client: FluenceClient, id: string, nodeId?: string, ttl?: number): Promise<void> => {
let returnValue = 'empty';
let call = (nodeId: string) => `(call "${nodeId}" ("script" "remove") [script_id] ${returnValue})`;
let data = new Map();
data.set('script_id', id);
return requestResponse(client, 'removeScript', call, returnValue, data, (args) => {}, nodeId, ttl);
};

View File

@ -14,4 +14,44 @@
* limitations under the License.
*/
import { SecurityTetraplet } from '@fluencelabs/avm';
/**
* Peer ID's id as a base58 string (multihash/CIDv0).
*/
export type PeerIdB58 = string;
/**
* Additional information about a service call
*/
export interface CallParams<ArgName extends string | null> {
/**
* 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: { [key in ArgName]: SecurityTetraplet[] };
}

View File

@ -0,0 +1,5 @@
export { FluencePeer } from '../FluencePeer';
export { ResultCodes } from '../../internal/CallServiceHandler';
export { RequestFlow } from '../../internal/RequestFlow';
export { RequestFlowBuilder } from '../../internal/RequestFlowBuilder';
export { CallParams } from '../commonTypes';

View File

@ -4,9 +4,9 @@ import {
CallServiceHandler,
CallServiceResult,
CallServiceResultType,
errorHandler,
Middleware,
} from './CallServiceHandler';
import { errorHandler } from './defaultMiddlewares';
const makeDefaultClientHandler = (): CallServiceHandler => {
const success = (resp: CallServiceResult, result: CallServiceResultType) => {

View File

@ -0,0 +1,14 @@
import { CallServiceArg, CallServiceData, CallServiceResult, Middleware, ResultCodes } from './CallServiceHandler';
/**
* Error catching middleware
*/
export const errorHandler: Middleware = (req: CallServiceData, resp: CallServiceResult, next: Function): void => {
try {
next();
} catch (e) {
resp.retCode = ResultCodes.exceptionInHandler;
resp.result = `Handler failed. fnName="${req.fnName}" serviceId="${req.serviceId}" error: ${e.toString()}`;
}
};
1;

View File

@ -1,48 +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 * as PeerId from 'peer-id';
import { decode, encode } from 'bs58';
import { keys } from 'libp2p-crypto';
/**
* Converts seed string which back to peer id. Seed string can be obtained by using @see {@link peerIdToSeed} function
* @param { string } seed - Seed to convert to peer id
* @returns { PeerId } - Peer id
*/
export const seedToPeerId = async (seed: string): Promise<PeerId> => {
const seedArr = decode(seed);
const privateKey = await keys.generateKeyPairFromSeed('Ed25519', Uint8Array.from(seedArr), 256);
return await PeerId.createFromPrivKey(privateKey.bytes);
};
/**
* Converts peer id to a string which can be used to restore back to peer id format with. @see {@link seedToPeerId}
* @param { PeerId } peerId - Peer id to convert to seed
* @returns { string } - Seed string
*/
export const peerIdToSeed = (peerId: PeerId): string => {
const seedBuf = peerId.privKey.marshal().subarray(0, 32);
return encode(seedBuf);
};
/**
* Generates a new peer id with random private key
* @returns { Promise<PeerId> } - Promise with the created Peer Id
*/
export const generatePeerId = async (): Promise<PeerId> => {
return await PeerId.create({ keyType: 'Ed25519' });
};

72
src/internal/utils.ts Normal file
View File

@ -0,0 +1,72 @@
import { AirInterpreter, LogLevel as AvmLogLevel } from '@fluencelabs/avm';
import log from 'loglevel';
import { AvmLoglevel, FluencePeer } from './FluencePeer';
import { RequestFlowBuilder } from './RequestFlowBuilder';
export const createInterpreter = (handler, peerId, logLevel: AvmLoglevel): Promise<AirInterpreter> => {
const logFn = (level: AvmLogLevel, msg: string) => {
switch (level) {
case 'error':
log.error(msg);
break;
case 'warn':
log.warn(msg);
break;
case 'info':
log.info(msg);
break;
case 'debug':
case 'trace':
log.log(msg);
break;
}
};
return AirInterpreter.create(handler, peerId, logLevel, logFn);
};
/**
* Checks the network connection by sending a ping-like request to relat node
* @param { FluenceClient } peer - The Fluence Client instance.
*/
export const checkConnection = async (peer: FluencePeer, ttl?: number): Promise<boolean> => {
if (!peer.connectionInfo.isConnected) {
return false;
}
const msg = Math.random().toString(36).substring(7);
const callbackFn = 'checkConnection';
const callbackService = '_callback';
const [request, promise] = new RequestFlowBuilder()
.withRawScript(
`(seq
(call init_relay ("op" "identity") [msg] result)
(call %init_peer_id% ("${callbackService}" "${callbackFn}") [result])
)`,
)
.withTTL(ttl)
.withVariables({
msg,
})
.buildAsFetch<[string]>(callbackService, callbackFn);
await peer.internals.initiateFlow(request);
try {
const [result] = await promise;
if (result != msg) {
log.warn("unexpected behavior. 'identity' must return the passed arguments.");
}
return true;
} catch (e) {
log.error('Error on establishing connection: ', e);
return false;
}
};
export const ParticleDataToString = (data: Uint8Array): string => {
return new TextDecoder().decode(Buffer.from(data));
};

View File

@ -6,6 +6,7 @@
],
"outDir": "./dist/",
"baseUrl": ".",
"downlevelIteration": true,
"sourceMap": true,
"inlineSources": true,
"strictFunctionTypes": true,