Client particle (#949)

This commit is contained in:
Dima 2020-09-28 17:01:49 +03:00 committed by GitHub
parent 85587c087d
commit 98e20c0ff5
13 changed files with 279 additions and 1234 deletions

View File

@ -1,85 +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.
*/
export interface Interface {
service_id: string,
modules: Module[]
}
export function checkFunction(f: any): f is Function {
if (!f.name) throw Error(`There is no 'name' field in Function struct: ${JSON.stringify(f)}`)
if (f.input_types) {
if (!(f.input_types instanceof Array)) {
throw Error(`'input_types' should be an array: ${JSON.stringify(f)}`)
}
f.input_types.forEach((i: any) => {
if ((typeof i) !== 'string') {
throw Error(`'input_types' should be a string: ${JSON.stringify(f)}`)
}
});
}
if (f.output_types) {
if (!(f.output_types instanceof Array)) {
throw Error(`'output_types' should be an array: ${JSON.stringify(f)}`)
}
f.output_types.forEach((o: any) => {
if ((typeof o) !== 'string') {
throw Error(`'output_types' should be a string: ${JSON.stringify(f)}`)
}
});
}
return true;
}
function checkModule(module: any): module is Module {
if (!module.name) throw Error(`There is no 'name' field in Module struct: ${JSON.stringify(module)}`)
if (!module.functions) {
module.functions.forEach((f: any) => {
checkFunction(f)
});
}
return true;
}
/**
* Throws an error if 'i' is not an Interface type.
*/
export function checkInterface(i: any): i is Interface {
if (!i.service_id) throw new Error(`There is no 'service_id' field in Interface struct: ${JSON.stringify(i)}`)
if (i.modules) {
i.modules.forEach((module: any) => {
checkModule(module);
});
}
return true;
}
export interface Module {
name: string,
functions: Function[]
}
export interface Function {
name: string,
input_types: string[],
output_types: string[]
}

View File

@ -1,176 +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 {encode} from "bs58"
export interface Address {
protocols: Protocol[],
hash?: string
}
export interface Protocol {
protocol: ProtocolType,
value?: string
}
export enum ProtocolType {
Providers = "providers",
Peer = "peer",
Signature = "signature",
Client = "client"
}
const PROTOCOL = "fluence:";
export function getSignature(address: Address): string | undefined {
return address.protocols.find(p => p.protocol === ProtocolType.Signature)?.value
}
export function addressToString(address: Address): string {
let addressStr = PROTOCOL;
for (let addr of address.protocols) {
addressStr = addressStr + "/" + addr.protocol;
if (addr.value) {
addressStr = addressStr + "/" + addr.value;
}
}
if (address.hash) {
addressStr = addressStr + "#" + address.hash
}
return addressStr;
}
function protocolWithValue(protocol: ProtocolType, protocolIterator: IterableIterator<[number, string]>): Protocol {
let protocolValue = protocolIterator.next().value;
if (!protocolValue || !protocolValue[1]) {
throw Error(`protocol '${protocol}' should be with a value`)
}
return {protocol: protocol, value: protocolValue[1]};
}
export function parseProtocol(protocol: string, protocolIterator: IterableIterator<[number, string]>): Protocol {
protocol = protocol.toLocaleLowerCase();
switch (protocol) {
case ProtocolType.Providers:
return protocolWithValue(protocol, protocolIterator);
case ProtocolType.Client:
return protocolWithValue(protocol, protocolIterator);
case ProtocolType.Peer:
return protocolWithValue(protocol, protocolIterator);
case ProtocolType.Signature:
return protocolWithValue(protocol, protocolIterator);
default:
throw Error("cannot parse protocol. Should be 'service|peer|client|signature'");
}
}
export async function createRelayAddressWithSig(relay: string, peerId: PeerId, sig: string, hash?: string): Promise<Address> {
let protocols = [
{protocol: ProtocolType.Peer, value: relay},
{protocol: ProtocolType.Client, value: peerId.toB58String()},
{protocol: ProtocolType.Signature, value: sig}
];
return {
protocols: protocols,
hash: hash
}
}
export async function createRelayAddress(relay: string, peerId: PeerId, withSig: boolean, hash?: string): Promise<Address> {
let protocols = [
{protocol: ProtocolType.Peer, value: relay},
{protocol: ProtocolType.Client, value: peerId.toB58String()}
];
if (withSig) {
let str = addressToString({protocols: protocols}).replace(PROTOCOL, "");
let signature = await peerId.privKey.sign(Buffer.from(str));
let signatureStr = encode(signature);
protocols.push({protocol: ProtocolType.Signature, value: signatureStr});
}
return {
protocols: protocols,
hash: hash
}
}
export function createProviderAddress(service: string, hash?: string): Address {
let protocol = {protocol: ProtocolType.Providers, value: service};
return {
protocols: [protocol],
hash: hash
}
}
export function createPeerAddress(peer: string, hash?: string): Address {
let protocol = {protocol: ProtocolType.Peer, value: peer};
return {
protocols: [protocol],
hash: hash
}
}
export function parseAddress(str: string): Address {
str = str.replace("fluence:", "");
// delete leading slashes
str = str.replace(/^\/+/, '');
let mainAndHash = str.split("#");
let parts = mainAndHash[0].split("/");
if (parts.length < 1) {
throw Error("address parts should not be empty")
}
let protocols: Protocol[] = [];
let partsEntries: IterableIterator<[number, string]> = parts.entries();
while (true) {
let result = partsEntries.next();
if (result.done) break;
let protocol = parseProtocol(result.value[1], partsEntries);
protocols.push(protocol);
}
let hashPart = mainAndHash.slice(1, mainAndHash.length).join();
let hash = undefined;
if (hashPart) {
hash = hashPart;
}
return {
protocols: protocols,
hash: hash
}
}

View File

@ -1,35 +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.
*/
export interface Blueprint {
dependencies: string[],
id: string,
name: string
}
export function checkBlueprint(b: any): b is Blueprint {
if (!b.id) throw new Error(`There is no 'id' field in Blueprint struct: ${JSON.stringify(b)}`)
if (b.dependencies) {
b.dependencies.forEach((dep: any) => {
if ((typeof dep) !== 'string') {
throw Error(`'dependencies' should be an array of strings: ${JSON.stringify(b)}`)
}
});
}
return true;
}

View File

@ -14,398 +14,54 @@
* limitations under the License. * limitations under the License.
*/ */
import {
Address,
createPeerAddress,
createRelayAddress,
createProviderAddress,
ProtocolType,
addressToString, createRelayAddressWithSig
} from "./address";
import {callToString, FunctionCall, genUUID, makeFunctionCall,} from "./functionCall";
import * as PeerId from "peer-id";
import {LocalServices} from "./localServices";
import Multiaddr from "multiaddr"
import {Subscriptions} from "./subscriptions";
import {FluenceConnection} from "./fluenceConnection";
import {checkInterface, Interface} from "./Interface";
import {Service} from "./service";
import {Blueprint, checkBlueprint} from "./blueprint";
import * as log from 'loglevel';
/** import {Particle} from "./particle";
* @param target receiver import * as PeerId from "peer-id";
* @param args message in the call import Multiaddr from "multiaddr"
* @param moduleId module name import {FluenceConnection} from "./fluenceConnection";
* @param fname function name import {Subscriptions} from "./subscriptions";
* @param name common field for debug purposes
* @param msgId hash that will be added to replyTo address
*/
interface Call {
target: Address,
args: any,
moduleId?: string,
fname?: string,
msgId?: string,
name?: string
}
export class FluenceClient { export class FluenceClient {
readonly selfPeerId: PeerId; readonly selfPeerId: PeerId;
readonly selfPeerIdStr: string; readonly selfPeerIdStr: string;
private nodePeerIdStr: string; private nodePeerIdStr: string;
private subscriptions = new Subscriptions();
connection: FluenceConnection; connection: FluenceConnection;
private services: LocalServices = new LocalServices();
private subscriptions: Subscriptions = new Subscriptions();
constructor(selfPeerId: PeerId) { constructor(selfPeerId: PeerId) {
this.selfPeerId = selfPeerId; this.selfPeerId = selfPeerId;
this.selfPeerIdStr = selfPeerId.toB58String(); this.selfPeerIdStr = selfPeerId.toB58String();
} }
/**
* Makes call with response from function. Without reply_to field.
*/
private responseCall(target: Address, args: any): FunctionCall {
return makeFunctionCall(genUUID(), target, this.connection.sender, args, undefined, "response");
}
/** /**
* Waits a response that match the predicate. * Waits a response that match the predicate.
* *
* @param predicate will be applied to each incoming call until it matches * @param id
* @param ignoreErrors ignore an errors, wait for success response * @param ttl
*/ */
waitResponse(predicate: (args: any, target: Address, replyTo: Address, moduleId?: string, fname?: string) => (boolean | undefined), ignoreErrors: boolean): Promise<any> { waitResponse(id: string, ttl: number): Promise<Particle> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// subscribe for responses, to handle response // subscribe for responses, to handle response
// TODO if there's no conn, reject // TODO if there's no conn, reject
this.subscribe((args: any, target: Address, replyTo: Address, moduleId?: string, fname?: string) => { this.subscriptions.subscribe(id, (particle: Particle) => {
if (predicate(args, target, replyTo, moduleId, fname)) { resolve(particle);
if (args.reason) { }, ttl);
if (ignoreErrors) { })
return false;
} else {
reject(new Error(args.reason));
}
} else {
resolve(args);
}
return true;
}
return false;
});
});
}
private getPredicate(msgId: string): (args: any, target: Address) => (boolean | undefined) {
return (args: any, target: Address) => target.hash && target.hash === msgId;
}
/**
* Send call and forget.
*
*/
async sendCall(call: Call) {
if (this.connection && this.connection.isConnected()) {
await this.connection.sendFunctionCall(call.target, call.args, call.moduleId, call.fname, call.msgId, call.name);
} else {
throw Error("client is not connected")
}
}
/**
* Send call to the provider and wait a response matches predicate.
*
* @param provider published name in dht
* @param args message to the service
* @param moduleId module name
* @param fname function name
* @param name debug info
*/
async callProvider(provider: string, args: any, moduleId?: string, fname?: string, name?: string): Promise<any> {
let msgId = genUUID();
let predicate = this.getPredicate(msgId);
let address = createProviderAddress(provider);
await this.sendCall({target: address, args: args, moduleId: moduleId, fname: fname, msgId: msgId, name: name});
return await this.waitResponse(predicate, true);
}
/**
* Send a message to a client that connected with a relay.
*
* @param relayId
* @param clientId
* @param sig
* @param moduleId
* @param args message to the service
* @param fname function name
* @param name debug info
*/
async callClient(relayId: string, clientId: string, sig: string, moduleId: string, args: any, fname?: string, name?: string): Promise<void> {
let msgId = genUUID();
let clientPeerId = await PeerId.createFromB58String(clientId);
let address = await createRelayAddressWithSig(relayId, clientPeerId, sig);
await this.sendCall({target: address, args: args, moduleId: moduleId, fname: fname, msgId: msgId, name: name})
}
/**
* Send a call to the local service and wait a response matches predicate on a peer the client connected with.
*
* @param moduleId
* @param addr node address
* @param args message to the service
* @param fname function name
* @param name debug info
*/
async callPeer(moduleId: string, args: any, fname?: string, addr?: string, name?: string): Promise<any> {
let msgId = genUUID();
let predicate = this.getPredicate(msgId);
let address;
if (addr) {
address = createPeerAddress(addr);
} else {
address = createPeerAddress(this.nodePeerIdStr);
}
await this.sendCall({target: address, args: args, moduleId: moduleId, fname: fname, msgId: msgId, name: name})
return await this.waitResponse(predicate, false);
}
async callService(peerId: string, serviceId: string, moduleId: string, args: any, fname?: string): Promise<any> {
let target = createPeerAddress(peerId, serviceId);
let msgId = genUUID();
let predicate = this.getPredicate(msgId);
await this.sendCall({target: target, args: args, moduleId: moduleId, fname: fname, msgId: msgId});
return await this.waitResponse(predicate, false);
}
getService(peerId: string, serviceId: string): Service {
return new Service(this, peerId, serviceId);
} }
/** /**
* Handle incoming call. * Handle incoming call.
* If FunctionCall returns - we should send it as a response. * If FunctionCall returns - we should send it as a response.
*/ */
handleCall(): (call: FunctionCall) => FunctionCall | undefined { handleParticle(): (particle: Particle) => void {
let _this = this; let _this = this;
return (call: FunctionCall) => { return (particle: Particle) => {
log.debug("FunctionCall received:");
// if other side return an error - handle it
// TODO do it in the protocol
/*if (call.arguments.error) {
this.handleError(call);
} else {
}*/
let target = call.target;
// the tail of addresses should be you or your service
let lastProtocol = target.protocols[target.protocols.length - 1];
// call all subscriptions for a new call // call all subscriptions for a new call
_this.subscriptions.applyToSubscriptions(call); _this.subscriptions.applyToSubscriptions(particle);
switch (lastProtocol.protocol) {
case ProtocolType.Providers:
return undefined;
case ProtocolType.Client:
if (lastProtocol.value === _this.selfPeerIdStr) {
log.debug(`relay call:`);
log.debug(JSON.stringify(call, undefined, 2));
if (call.module) {
try {
// call of the service, service should handle response sending, error handling, requests to other services
let applied = _this.services.applyToService(call);
// if the request hasn't been applied, there is no such service. Return an error.
if (!applied) {
log.warn(`there is no service ${lastProtocol.value}`);
return this.responseCall(call.reply_to, {
reason: `there is no such service`,
msg: call
});
}
} catch (e) {
// if service throw an error, return it to the sender
return this.responseCall(call.reply_to, {
reason: `error on execution: ${e}`,
msg: call
});
}
}
} else {
log.warn(`this relay call is not for me: ${callToString(call)}`);
return this.responseCall(call.reply_to, {
reason: `this relay call is not for me`,
msg: call
});
}
return undefined;
case ProtocolType.Peer:
if (lastProtocol.value === this.selfPeerIdStr) {
log.debug(`peer call: ${call}`);
} else {
log.warn(`this peer call is not for me: ${callToString(call)}`);
return this.responseCall(call.reply_to, {
reason: `this relay call is not for me`,
msg: call
});
}
return undefined;
}
}
}
/**
* Become a name provider. Other network members could find and call one of the providers of this name by this name.
*/
async provideName(name: string, fn: (req: FunctionCall) => void) {
let replyTo = this.connection.sender;
await this.callPeer("provide", {name: name, address: addressToString(replyTo)})
this.services.addService(name, fn);
}
/**
* Sends a call to create a service on remote node.
*/
async createService(blueprint: string, peerId?: string): Promise<string> {
let resp = await this.callPeer("create", {blueprint_id: blueprint}, undefined, peerId);
if (resp && resp.service_id) {
return resp.service_id
} else {
log.error("Unknown response type on `createService`: ", resp)
throw new Error("Unknown response type on `createService`");
}
}
async addBlueprint(name: string, dependencies: string[], peerId?: string): Promise<string> {
let id = genUUID();
let blueprint = {
name: name,
id: id,
dependencies: dependencies
};
let msg_id = genUUID();
await this.callPeer("add_blueprint", {msg_id, blueprint}, undefined, peerId);
return id;
}
async getInterface(serviceId: string, peerId?: string): Promise<Interface> {
let resp;
resp = await this.callPeer("get_interface", {service_id: serviceId}, undefined, peerId)
let i = resp.interface;
if (checkInterface(i)) {
return i;
} else {
throw new Error("Unexpected");
}
}
async getAvailableBlueprints(peerId?: string): Promise<Blueprint[]> {
let resp = await this.callPeer("get_available_blueprints", {}, undefined, peerId);
let blueprints = resp.available_blueprints;
if (blueprints && blueprints instanceof Array) {
return blueprints.map((b: any) => {
if (checkBlueprint(b)) {
return b;
} else {
throw new Error("Unexpected");
}
});
} else {
throw new Error("Unexpected. 'get_active_interfaces' should return an array of interfaces.");
}
}
async getActiveInterfaces(peerId?: string): Promise<Interface[]> {
let resp = await this.callPeer("get_active_interfaces", {}, undefined, peerId);
let interfaces = resp.active_interfaces;
if (interfaces && interfaces instanceof Array) {
return interfaces.map((i: any) => {
if (checkInterface(i)) {
return i;
} else {
throw new Error("Unexpected");
}
});
} else {
throw new Error("Unexpected. 'get_active_interfaces' should return an array pf interfaces.");
}
}
async getAvailableModules(peerId?: string): Promise<string[]> {
let resp = await this.callPeer("get_available_modules", {}, undefined, peerId);
return resp.available_modules;
}
/**
* Add new WASM module to the node.
*
* @param bytes WASM in base64
* @param name WASM identificator
* @param mem_pages_count memory amount for WASM
* @param envs environment variables
* @param mapped_dirs links to directories
* @param preopened_files files and directories that will be used in WASM
* @param peerId the node to add module
*/
async addModule(bytes: string, name: string, mem_pages_count: number, envs: string[], mapped_dirs: any, preopened_files: string[], peerId?: string): Promise<any> {
let config: any = {
logger_enabled: true,
mem_pages_count: mem_pages_count,
name: name,
wasi: {
envs: envs,
mapped_dirs: mapped_dirs,
preopened_files: preopened_files
}
}
let resp = await this.callPeer("add_module", {bytes: bytes, config: config}, undefined, peerId);
return resp.available_modules;
}
// subscribe new hook for every incoming call, to handle in-service responses and other different cases
// the hook will be deleted if it will return `true`
subscribe(predicate: (args: any, target: Address, replyTo: Address, moduleId?: string, fname?: string) => (boolean | undefined)) {
this.subscriptions.subscribe(predicate)
}
/**
* Sends a call to unregister the service.
*/
async unregisterService(moduleId: string) {
if (this.services.deleteService(moduleId)) {
log.warn("unregister is not implemented yet (service: ${serviceId}")
// TODO unregister in fluence network when it will be supported
// let regMsg = makeRegisterMessage(serviceId, PeerId.createFromB58String(this.nodePeerId));
// await this.sendFunctionCall(regMsg);
} }
} }
@ -436,21 +92,15 @@ export class FluenceClient {
} }
let peerId = PeerId.createFromB58String(nodePeerId); let peerId = PeerId.createFromB58String(nodePeerId);
let sender = await createRelayAddress(nodePeerId, this.selfPeerId, false); let connection = new FluenceConnection(multiaddr, peerId, this.selfPeerId, this.handleParticle());
let replyTo = await createRelayAddress(nodePeerId, this.selfPeerId, true);
let connection = new FluenceConnection(multiaddr, peerId, this.selfPeerId, sender, replyTo, this.handleCall());
await connection.connect(); await connection.connect();
this.connection = connection; this.connection = connection;
// if the client already had a connection, it will reregister all services after establishing a new connection.
if (!firstConnection) {
for (let service of this.services.getAllServices().keys()) {
await this.connection.provideName(service);
}
} }
async sendParticle(particle: Particle): Promise<Particle> {
await this.connection.sendParticle(particle);
return this.waitResponse(particle.id, particle.ttl);
} }
} }

View File

@ -14,16 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import {Address, createPeerAddress} from "./address";
import {
callToString,
FunctionCall,
genUUID,
makeFunctionCall,
makeProvideMessage,
parseFunctionCall
} from "./functionCall";
import Websockets from "libp2p-websockets"; import Websockets from "libp2p-websockets";
import Mplex from "libp2p-mplex"; import Mplex from "libp2p-mplex";
import SECIO from "libp2p-secio"; import SECIO from "libp2p-secio";
@ -33,6 +23,7 @@ import pipe from "it-pipe";
import Multiaddr from "multiaddr"; import Multiaddr from "multiaddr";
import PeerId from "peer-id"; import PeerId from "peer-id";
import * as log from 'loglevel'; import * as log from 'loglevel';
import {parseParticle, Particle, stringifyParticle} from "./particle";
export const PROTOCOL_NAME = '/fluence/faas/1.0.0'; export const PROTOCOL_NAME = '/fluence/faas/1.0.0';
@ -45,32 +36,19 @@ enum Status {
export class FluenceConnection { export class FluenceConnection {
private readonly selfPeerId: PeerId; private readonly selfPeerId: PeerId;
readonly sender: Address; readonly relay: PeerId;
readonly replyTo: Address;
private node: LibP2p; private node: LibP2p;
private readonly address: Multiaddr; private readonly address: Multiaddr;
readonly nodePeerId: PeerId; readonly nodePeerId: PeerId;
private readonly selfPeerIdStr: string; private readonly selfPeerIdStr: string;
private readonly handleCall: (call: FunctionCall) => FunctionCall | undefined; private readonly handleCall: (call: Particle) => void;
constructor(multiaddr: Multiaddr, hostPeerId: PeerId, selfPeerId: PeerId, sender: Address, replyTo: Address, handleCall: (call: FunctionCall) => FunctionCall | undefined) { constructor(multiaddr: Multiaddr, hostPeerId: PeerId, selfPeerId: PeerId, handleCall: (call: Particle) => void) {
this.selfPeerId = selfPeerId; this.selfPeerId = selfPeerId;
this.handleCall = handleCall; this.handleCall = handleCall;
this.selfPeerIdStr = selfPeerId.toB58String(); this.selfPeerIdStr = selfPeerId.toB58String();
this.address = multiaddr; this.address = multiaddr;
this.nodePeerId = hostPeerId; this.nodePeerId = hostPeerId;
this.sender = sender
this.replyTo = replyTo
}
makeReplyTo(reply?: string): Address {
if (reply) {
let replyToWithHash = {...this.replyTo}
if (typeof reply === "string") replyToWithHash.hash = reply;
return replyToWithHash;
} else {
return this.sender;
}
} }
async connect() { async connect() {
@ -114,13 +92,8 @@ export class FluenceConnection {
for await (const msg of source) { for await (const msg of source) {
try { try {
log.debug(_this.selfPeerIdStr); log.debug(_this.selfPeerIdStr);
let call = parseFunctionCall(msg); let particle = parseParticle(msg);
let response = _this.handleCall(call); _this.handleCall(particle);
// send a response if it exists, do nothing otherwise
if (response) {
await _this.sendCall(response);
}
} catch(e) { } catch(e) {
log.error("error on handling a new incoming message: " + e); log.error("error on handling a new incoming message: " + e);
} }
@ -146,39 +119,20 @@ export class FluenceConnection {
this.status = Status.Disconnected; this.status = Status.Disconnected;
} }
private async sendCall(call: FunctionCall) { async sendParticle(particle: Particle): Promise<void> {
let callStr = callToString(call); this.checkConnectedOrThrow();
log.debug("send function call: " + JSON.stringify(JSON.parse(callStr), undefined, 2));
log.debug(call); let particleStr = stringifyParticle(particle);
log.debug("send function call: \n" + JSON.stringify(particle, undefined, 2));
// create outgoing substream // create outgoing substream
const conn = await this.node.dialProtocol(this.address, PROTOCOL_NAME) as {stream: Stream; protocol: string}; const conn = await this.node.dialProtocol(this.address, PROTOCOL_NAME) as {stream: Stream; protocol: string};
pipe( pipe(
[callStr], [particleStr],
// at first, make a message varint // at first, make a message varint
encode(), encode(),
conn.stream.sink, conn.stream.sink,
); );
} }
/**
* Send FunctionCall to the connected node.
*/
async sendFunctionCall(target: Address, args: any, moduleId?: string, fname?: string, msgId?: string, name?: string) {
this.checkConnectedOrThrow();
let replyTo;
if (msgId) replyTo = this.makeReplyTo(msgId);
let call = makeFunctionCall(genUUID(), target, this.sender, args, moduleId, fname, replyTo, name);
await this.sendCall(call);
}
async provideName(name: string) {
let target = createPeerAddress(this.nodePeerId.toB58String())
let regMsg = await makeProvideMessage(name, target, this.sender, this.replyTo);
await this.sendCall(regMsg);
}
} }

View File

@ -1,106 +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 {
Address, addressToString, parseAddress
} from "./address";
import { v4 as uuidv4 } from 'uuid';
import * as log from 'loglevel';
export interface FunctionCall {
uuid: string,
target: Address,
reply_to?: Address,
sender: Address,
"module"?: string,
fname?: string,
arguments: any,
name?: string,
action: "FunctionCall"
}
export function callToString(call: FunctionCall) {
let obj: any = {...call};
if (obj.reply_to) {
obj.reply_to = addressToString(obj.reply_to);
}
obj.target = addressToString(obj.target);
obj.sender = addressToString(obj.sender);
return JSON.stringify(obj)
}
export function makeFunctionCall(uuid: string, target: Address, sender: Address, args: object, moduleId?: string, fname?: string, replyTo?: Address, name?: string): FunctionCall {
return {
uuid: uuid,
target: target,
reply_to: replyTo,
sender: sender,
"module": moduleId,
fname: fname,
arguments: args,
name: name,
action: "FunctionCall"
}
}
export function parseFunctionCall(str: string): FunctionCall {
let json = JSON.parse(str);
log.debug(JSON.stringify(json, undefined, 2));
let replyTo: Address;
if (json.reply_to) replyTo = parseAddress(json.reply_to);
if (!json.uuid) throw Error(`there is no 'uuid' field in json.\n${str}`);
if (!json.target) throw Error(`there is no 'uuid' field in json.\n${str}`);
if (!json.sender) throw Error(`there is no 'sender' field in json.\n${str}`);
let target = parseAddress(json.target);
let sender = parseAddress(json.sender);
return {
uuid: json.uuid,
target: target,
reply_to: replyTo,
sender: sender,
arguments: json.arguments,
"module": json.module,
fname: json.fname,
name: json.name,
action: "FunctionCall"
}
}
export function genUUID() {
return uuidv4();
}
/**
* Message to provide new name.
*/
export async function makeProvideMessage(name: string, target: Address, sender: Address, sigAddress: Address): Promise<FunctionCall> {
return makeFunctionCall(genUUID(), target, sender, {name: name, address: addressToString(sigAddress)}, "provide", undefined, sender, "provide service_id");
}
// TODO uncomment when this will be implemented in Fluence network
/*export function makeUnregisterMessage(serviceId: string, peerId: PeerId): FunctionCall {
let target = createPeerAddress(peerId.toB58String());
return makeFunctionCall(genUUID(), target, target, {key: serviceId}, undefined, "unregister");
}*/

View File

@ -1,49 +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 {FunctionCall} from "./functionCall";
export class LocalServices {
private services: Map<string, (req: FunctionCall) => void> = new Map();
constructor() {}
addService(serviceId: string, callback: (req: FunctionCall) => void): void {
this.services.set(serviceId, callback);
}
getAllServices(): Map<string, (req: FunctionCall) => void> {
return this.services;
}
deleteService(serviceId: string): boolean {
return this.services.delete(serviceId)
}
// could throw error from service callback
// returns true if the call was applied
applyToService(call: FunctionCall): boolean {
let service = this.services.get(call.module);
if (service) {
service(call);
return true;
} else {
return false;
}
}
}

113
src/particle.ts Normal file
View File

@ -0,0 +1,113 @@
/*
* 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 { v4 as uuidv4 } from 'uuid';
import PeerId from "peer-id";
import {encode} from "bs58";
const DEFAULT_TTL = 7000;
export interface Particle {
id: string,
init_peer_id: string,
timestamp: number,
ttl: number,
script: string,
// sign upper fields
signature: string,
data: object
}
export async function build(peerId: PeerId, script: string, data: object, ttl?: number): Promise<Particle> {
let id = genUUID();
let currentTime = (new Date()).getTime();
ttl = ttl ?? DEFAULT_TTL;
let particle: Particle = {
id: id,
init_peer_id: peerId.toB58String(),
timestamp: currentTime,
ttl: ttl,
script: script,
signature: "",
data: data
}
particle.signature = await signParticle(peerId, particle);
return particle;
}
/**
* Copies a particle and stringify it.
*/
export function stringifyParticle(call: Particle): string {
let obj: any = {...call};
obj.action = "Particle"
// delete it after signatures will be implemented on nodes
obj.signature = []
return JSON.stringify(obj)
}
export function parseParticle(str: string): Particle {
let json = JSON.parse(str);
return {
id: json.id,
init_peer_id: json.init_peer_id,
timestamp: json.timestamp,
ttl: json.ttl,
script: json.script,
signature: json.signature,
data: json.data
}
}
export function canonicalBytes(particle: Particle) {
let peerIdBuf = Buffer.from(particle.init_peer_id, 'utf8');
let idBuf = Buffer.from(particle.id, 'utf8');
let tsArr = new ArrayBuffer(8);
new DataView(tsArr).setBigUint64(0, BigInt(particle.timestamp));
let tsBuf = Buffer.from(tsArr);
let ttlArr = new ArrayBuffer(4);
new DataView(ttlArr).setUint32(0, particle.ttl);
let ttlBuf = Buffer.from(ttlArr);
let scriptBuf = Buffer.from(particle.script, 'utf8');
return Buffer.concat([peerIdBuf, idBuf, tsBuf, ttlBuf, scriptBuf]);
}
/**
* Sign a particle with a private key from peerId.
*/
export async function signParticle(peerId: PeerId,
particle: Particle): Promise<string> {
let bufToSign = canonicalBytes(particle);
let signature = await peerId.privKey.sign(bufToSign)
return encode(signature)
}
export function genUUID() {
return uuidv4();
}

View File

@ -1,39 +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";
export class Service {
private readonly client: FluenceClient;
private readonly serviceId: string;
private readonly peerId: string;
constructor(client: FluenceClient, peerId: string, serviceId: string) {
this.client = client;
this.serviceId = serviceId;
this.peerId = peerId;
}
/**
*
* @param moduleId wich module in service to call
* @param args parameters to call service
* @param fname function name if existed
*/
async call(moduleId: string, args: any, fname?: string): Promise<any> {
return this.client.callService(this.peerId, this.serviceId, moduleId, args, fname);
}
}

View File

@ -14,29 +14,45 @@
* limitations under the License. * limitations under the License.
*/ */
import {FunctionCall} from "./functionCall"; import {Particle} from "./particle";
import {Address} from "./address";
export class Subscriptions { export class Subscriptions {
private subscriptions: ((args: any, target: Address, replyTo: Address, module?: string, fname?: string) => (boolean | undefined))[] = []; private subscriptions: Map<string, (particle: Particle) => void> = new Map();
constructor() {} constructor() {}
/** /**
* Subscriptions will be applied to all peer and relay messages. * Subscriptions will be applied by outside message if id will be the same.
* If subscription returns true, delete subscription. *
* @param f * @param id message identificator
* @param f function to use with outside message
* @param ttl time to live, subscription will be deleted after this time
*/ */
subscribe(f: (args: any, target: Address, replyTo: Address, moduleId?: string, fname?: string) => (boolean | undefined)) { subscribe(id: string, f: (particle: Particle) => void, ttl: number) {
this.subscriptions.push(f); let _this = this;
setTimeout(() => {
_this.subscriptions.delete(id)
console.log(`Particle with id ${id} deleted by timeout`)
}, ttl)
this.subscriptions.set(id, f);
} }
/** /**
* Apply call to all subscriptions and delete subscriptions that return `true`. * A particle will be applied if id of the particle was subscribed earlier.
* @param call * @param particle
*/ */
applyToSubscriptions(call: FunctionCall) { applyToSubscriptions(particle: Particle) {
// if subscription return true - delete it from subscriptions // if subscription return true - delete it from subscriptions
this.subscriptions = this.subscriptions.filter(callback => !callback(call.arguments, call.target, call.reply_to, call.module, call.fname)) let callback = this.subscriptions.get(particle.id)
if (callback) {
callback(particle);
} else {
if (Number(particle.timestamp) + particle.ttl > Date.now()) {
console.log("Old particle received. 'ttl' is ended.");
} else {
console.log("External particle received. 'Stepper' needed on client. Unimplemented.");
}
console.log(particle);
}
} }
} }

View File

@ -1,298 +0,0 @@
import {
createPeerAddress,
createRelayAddress,
createProviderAddress,
addressToString,
parseAddress
} from "../address";
import {expect} from 'chai';
import 'mocha';
import {encode} from "bs58"
import * as PeerId from "peer-id";
import {callToString, genUUID, makeFunctionCall, parseFunctionCall} from "../functionCall";
import Fluence from "../fluence";
import {certificateFromString, certificateToString, issue} from "../trust/certificate";
import {TrustGraph} from "../trust/trust_graph";
import {nodeRootCert} from "../trust/misc";
import {peerIdToSeed, seedToPeerId} from "../seed";
import {greetingWASM} from "./greeting_wasm";
describe("Typescript usage suite", () => {
it("should throw an error, if protocol will be without value", () => {
expect(() => parseAddress("/peer/")).to.throw(Error);
});
it("should be able to convert service_id address to and from string", () => {
let addr = createProviderAddress("service_id-1");
let str = addressToString(addr);
let parsed = parseAddress(str);
expect(parsed).to.deep.equal(addr)
});
it("should be able to convert peer address to and from string", () => {
let pid = PeerId.createFromB58String("QmXduoWjhgMdx3rMZXR3fmkHKdUCeori9K1XkKpqeF5DrU");
let addr = createPeerAddress(pid.toB58String());
let str = addressToString(addr);
let parsed = parseAddress(str);
expect(parsed).to.deep.equal(addr)
});
it("should be able to convert relay address to and from string", async () => {
let pid = await PeerId.create();
let relayid = await PeerId.create();
let addr = await createRelayAddress(relayid.toB58String(), pid, true);
let str = addressToString(addr);
let parsed = parseAddress(str);
expect(parsed).to.deep.equal(addr)
});
it("should be able to convert function call to and from string", async () => {
let pid = await PeerId.create();
let relayid = await PeerId.create();
let addr = await createRelayAddress(relayid.toB58String(), pid, true);
let addr2 = createPeerAddress(pid.toB58String());
let functionCall = makeFunctionCall(
"123",
addr2,
addr2,
{
arg1: "123",
arg2: 3,
arg4: [1, 2, 3]
},
"mm",
"fff",
addr,
"2444"
);
let str = callToString(functionCall);
let parsed = parseFunctionCall(str);
expect(parsed).to.deep.equal(functionCall);
let functionCallWithOptional = makeFunctionCall(
"123",
addr,
addr,
{
arg1: "123",
arg2: 3,
arg4: [1, 2, 3]
}
);
let str2 = callToString(functionCallWithOptional);
let parsed2 = parseFunctionCall(str2);
expect(parsed2).to.deep.equal(functionCallWithOptional)
});
it("should create private key from seed and back", async function () {
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)
console.log("SEED STR: " + seedStr)
let pid = await seedToPeerId(seedStr)
expect(peerIdToSeed(pid)).to.be.equal(seedStr)
})
it("should serialize and deserialize certificate correctly", async function () {
let cert = `11
1111
5566Dn4ZXXbBK5LJdUsE7L3pG9qdAzdPY47adjzkhEx9
3HNXpW2cLdqXzf4jz5EhsGEBFkWzuVdBCyxzJUZu2WPVU7kpzPjatcqvdJMjTtcycVAdaV5qh2fCGphSmw8UMBkr
158981172690500
1589974723504
2EvoZAZaGjKWFVdr36F1jphQ5cW7eK3yM16mqEHwQyr7
4UAJQWzB3nTchBtwARHAhsn7wjdYtqUHojps9xV6JkuLENV8KRiWM3BhQByx5KijumkaNjr7MhHjouLawmiN1A4d
1590061123504
1589974723504`
let deser = await certificateFromString(cert);
let ser = certificateToString(deser);
expect(ser).to.be.equal(cert);
});
// delete `.skip` and run `npm run test` to check service's and certificate's api with Fluence nodes
it.skip("test provide", async function () {
this.timeout(15000);
await testProvide();
});
// delete `.skip` and run `npm run test` to check service's and certificate's api with Fluence nodes
it.skip("test certs", async function () {
this.timeout(15000);
await testCerts();
});
// delete `.skip` and run `npm run test` to check service's and certificate's api with Fluence nodes
it.skip("test upload wasm", async function () {
this.timeout(15000);
await testUploadWasm();
});
// delete `.skip` and run `npm run test` to check service's and certificate's api with Fluence nodes
it.skip("test list of services and interfaces", async function () {
this.timeout(15000);
await testServicesAndInterfaces();
});
});
const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
export async function testCerts() {
let key1 = await Fluence.generatePeerId();
let key2 = await Fluence.generatePeerId();
// connect to two different nodes
let cl1 = await Fluence.connect("/dns4/134.209.186.43/tcp/9003/ws/p2p/12D3KooWBUJifCTgaxAUrcM9JysqCcS4CS8tiYH5hExbdWCAoNwb", key1);
let cl2 = await Fluence.connect("/ip4/134.209.186.43/tcp/9002/ws/p2p/12D3KooWHk9BjDQBUqnavciRPhAYFvqKBe4ZiPPvde7vDaqgn5er", key2);
let trustGraph1 = new TrustGraph(cl1);
let trustGraph2 = new TrustGraph(cl2);
let issuedAt = new Date();
let expiresAt = new Date();
// certificate expires after one day
expiresAt.setDate(new Date().getDate() + 1);
// create root certificate for key1 and extend it with key2
let rootCert = await nodeRootCert(key1);
let extended = await issue(key1, key2, rootCert, expiresAt.getTime(), issuedAt.getTime());
// publish certificates to Fluence network
await trustGraph1.publishCertificates(key2.toB58String(), [extended]);
// get certificates from network
let certs = await trustGraph2.getCertificates(key2.toB58String());
// root certificate could be different because nodes save trusts with bigger `expiresAt` date and less `issuedAt` date
expect(certs[0].chain[1].issuedFor.toB58String()).to.be.equal(extended.chain[1].issuedFor.toB58String())
expect(certs[0].chain[1].signature).to.be.equal(extended.chain[1].signature)
expect(certs[0].chain[1].expiresAt).to.be.equal(extended.chain[1].expiresAt)
expect(certs[0].chain[1].issuedAt).to.be.equal(extended.chain[1].issuedAt)
await cl1.disconnect();
await cl2.disconnect();
}
export async function testUploadWasm() {
let key1 = await Fluence.generatePeerId();
let cl1 = await Fluence.connect("/dns4/134.209.186.43/tcp/9100/ws/p2p/12D3KooWPnLxnY71JDxvB3zbjKu9k1BCYNthGZw6iGrLYsR1RnWM", key1);
let moduleName = genUUID()
await cl1.addModule(greetingWASM, moduleName, 100, [], {}, []);
let availableModules = await cl1.getAvailableModules();
console.log(availableModules);
let peerId1 = "12D3KooWPnLxnY71JDxvB3zbjKu9k1BCYNthGZw6iGrLYsR1RnWM"
let blueprintId = await cl1.addBlueprint("some test blueprint", [moduleName], peerId1)
let blueprints = await cl1.getAvailableBlueprints(peerId1)
console.log(blueprints);
let serviceId = await cl1.createService(peerId1, blueprintId);
let service = cl1.getService(peerId1, serviceId);
let argName = genUUID();
let resp = await service.call(moduleName, {name: argName}, "greeting")
expect(resp.result).to.be.equal(`Hi, ${argName}`)
await cl1.disconnect();
}
export async function testServicesAndInterfaces() {
let key1 = await Fluence.generatePeerId();
let key2 = await Fluence.generatePeerId();
// connect to two different nodes
let cl1 = await Fluence.connect("/dns4/134.209.186.43/tcp/9100/ws/p2p/12D3KooWPnLxnY71JDxvB3zbjKu9k1BCYNthGZw6iGrLYsR1RnWM", key1);
let cl2 = await Fluence.connect("/ip4/134.209.186.43/tcp/9002/ws/p2p/12D3KooWHk9BjDQBUqnavciRPhAYFvqKBe4ZiPPvde7vDaqgn5er", key2);
let peerId1 = "12D3KooWPnLxnY71JDxvB3zbjKu9k1BCYNthGZw6iGrLYsR1RnWM"
let blueprintId = await cl1.addBlueprint("some test blueprint", ["ipfs_node"], peerId1)
let serviceId = await cl2.createService(peerId1, blueprintId);
let resp = await cl2.callService(peerId1, serviceId, "ipfs_node", {}, "get_address")
console.log(resp)
let interfaces = await cl1.getActiveInterfaces();
let interfaceResp = await cl1.getInterface(serviceId, peerId1);
console.log(interfaces);
console.log(interfaceResp);
let availableModules = await cl1.getAvailableModules(peerId1);
console.log(availableModules);
await cl1.disconnect();
await cl2.disconnect();
}
// Shows how to register and call new service in Fluence network
export async function testProvide() {
let key1 = await Fluence.generatePeerId();
let key2 = await Fluence.generatePeerId();
// connect to two different nodes
let cl1 = await Fluence.connect("/dns4/134.209.186.43/tcp/9003/ws/p2p/12D3KooWBUJifCTgaxAUrcM9JysqCcS4CS8tiYH5hExbdWCAoNwb", key1);
let cl2 = await Fluence.connect("/ip4/134.209.186.43/tcp/9002/ws/p2p/12D3KooWHk9BjDQBUqnavciRPhAYFvqKBe4ZiPPvde7vDaqgn5er", key2);
// service name that we will register with one connection and call with another
let providerId = "sum-calculator-" + genUUID();
// register service that will add two numbers and send a response with calculation result
await cl1.provideName(providerId, async (req) => {
console.log("message received");
console.log(req);
console.log("send response");
let message = {msgId: req.arguments.msgId, result: req.arguments.one + req.arguments.two};
await cl1.sendCall({target: req.reply_to, args: message});
});
let req = {one: 12, two: 23};
// send call to `sum-calculator` service with two numbers
let response = await cl2.callProvider(providerId, req, providerId);
let result = response.result;
expect(result).to.be.equal(35)
await cl1.connect("/dns4/relay02.fluence.dev/tcp/19001/wss/p2p/12D3KooWEXNUbCXooUwHrHBbrmjsrpHXoEphPwbjQXEGyzbqKnE9");
await delay(1000);
// send call to `sum-calculator` service with two numbers
await cl2.callProvider(providerId, req, providerId, undefined, "calculator request");
let response2 = await cl2.callProvider(providerId, req, providerId);
let result2 = await response2.result;
console.log("RESULT:");
console.log(response2);
expect(result2).to.be.equal(35);
await cl1.disconnect();
await cl2.disconnect();
}

97
src/test/client.spec.ts Normal file
View File

@ -0,0 +1,97 @@
import {expect} from 'chai';
import 'mocha';
import {encode} from "bs58"
import Fluence from "../fluence";
import {certificateFromString, certificateToString, issue} from "../trust/certificate";
import {TrustGraph} from "../trust/trust_graph";
import {nodeRootCert} from "../trust/misc";
import {peerIdToSeed, seedToPeerId} from "../seed";
import {build, Particle} from "../particle";
describe("Typescript usage suite", () => {
it("should create private key from seed and back", async function () {
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)
console.log("SEED STR: " + seedStr)
let pid = await seedToPeerId(seedStr)
expect(peerIdToSeed(pid)).to.be.equal(seedStr)
})
it("should serialize and deserialize certificate correctly", async function () {
let cert = `11
1111
5566Dn4ZXXbBK5LJdUsE7L3pG9qdAzdPY47adjzkhEx9
3HNXpW2cLdqXzf4jz5EhsGEBFkWzuVdBCyxzJUZu2WPVU7kpzPjatcqvdJMjTtcycVAdaV5qh2fCGphSmw8UMBkr
158981172690500
1589974723504
2EvoZAZaGjKWFVdr36F1jphQ5cW7eK3yM16mqEHwQyr7
4UAJQWzB3nTchBtwARHAhsn7wjdYtqUHojps9xV6JkuLENV8KRiWM3BhQByx5KijumkaNjr7MhHjouLawmiN1A4d
1590061123504
1589974723504`
let deser = await certificateFromString(cert);
let ser = certificateToString(deser);
expect(ser).to.be.equal(cert);
});
it("test new client", async function () {
let key1 = await Fluence.generatePeerId();
let key2 = await Fluence.generatePeerId();
// connect to two different nodes
let cl1 = await Fluence.connect("/dns4/134.209.186.43/tcp/9003/ws/p2p/12D3KooWBUJifCTgaxAUrcM9JysqCcS4CS8tiYH5hExbdWCAoNwb", key1);
let particle = await build(key1, "123", {a: 777, b: "567"})
let result = await cl1.sendParticle(particle)
console.log(result)
});
// delete `.skip` and run `npm run test` to check service's and certificate's api with Fluence nodes
it.skip("test certs", async function () {
this.timeout(15000);
await testCerts();
});
});
const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
export async function testCerts() {
let key1 = await Fluence.generatePeerId();
let key2 = await Fluence.generatePeerId();
// connect to two different nodes
let cl1 = await Fluence.connect("/dns4/134.209.186.43/tcp/9003/ws/p2p/12D3KooWBUJifCTgaxAUrcM9JysqCcS4CS8tiYH5hExbdWCAoNwb", key1);
let cl2 = await Fluence.connect("/ip4/134.209.186.43/tcp/9002/ws/p2p/12D3KooWHk9BjDQBUqnavciRPhAYFvqKBe4ZiPPvde7vDaqgn5er", key2);
let trustGraph1 = new TrustGraph(cl1);
let trustGraph2 = new TrustGraph(cl2);
let issuedAt = new Date();
let expiresAt = new Date();
// certificate expires after one day
expiresAt.setDate(new Date().getDate() + 1);
// create root certificate for key1 and extend it with key2
let rootCert = await nodeRootCert(key1);
let extended = await issue(key1, key2, rootCert, expiresAt.getTime(), issuedAt.getTime());
// publish certificates to Fluence network
await trustGraph1.publishCertificates(key2.toB58String(), [extended]);
// get certificates from network
let certs = await trustGraph2.getCertificates(key2.toB58String());
// root certificate could be different because nodes save trusts with bigger `expiresAt` date and less `issuedAt` date
expect(certs[0].chain[1].issuedFor.toB58String()).to.be.equal(extended.chain[1].issuedFor.toB58String())
expect(certs[0].chain[1].signature).to.be.equal(extended.chain[1].signature)
expect(certs[0].chain[1].expiresAt).to.be.equal(extended.chain[1].expiresAt)
expect(certs[0].chain[1].issuedAt).to.be.equal(extended.chain[1].issuedAt)
await cl1.disconnect();
await cl2.disconnect();
}

View File

@ -18,6 +18,7 @@ import {FluenceClient} from "../fluenceClient";
import {Certificate, certificateFromString, certificateToString} from "./certificate"; import {Certificate, certificateFromString, certificateToString} from "./certificate";
import * as log from 'loglevel'; import * as log from 'loglevel';
// TODO update after 'aquamarine' implemented
// The client to interact with the Fluence trust graph API // The client to interact with the Fluence trust graph API
export class TrustGraph { export class TrustGraph {
@ -34,10 +35,11 @@ export class TrustGraph {
certsStr.push(await certificateToString(cert)); certsStr.push(await certificateToString(cert));
} }
let response = await this.client.callPeer("add_certificates", { /*let response = await this.client.callPeer("add_certificates", {
certificates: certsStr, certificates: certsStr,
peer_id: peerId peer_id: peerId
}); });*/
let response: any = {};
if (response.reason) { if (response.reason) {
throw Error(response.reason) throw Error(response.reason)
@ -50,9 +52,10 @@ export class TrustGraph {
// Get certificates that stores in Kademlia neighbourhood by `peerId` key. // Get certificates that stores in Kademlia neighbourhood by `peerId` key.
async getCertificates(peerId: string): Promise<Certificate[]> { async getCertificates(peerId: string): Promise<Certificate[]> {
let resp = await this.client.callPeer("certificates", { let resp: any = {};
/*let resp = await this.client.callPeer("certificates", {
peer_id: peerId peer_id: peerId
}); });*/
let certificatesRaw = resp.certificates let certificatesRaw = resp.certificates