mirror of
https://github.com/fluencelabs/fluence-js.git
synced 2025-06-21 03:41:32 +00:00
feat!: Standalone web JS Client (#243)
- Move marine-related part into FJS repo (fixes DXJ-184) - Move towards component-oriented architecture (fixes DXJ-183) - Different JS Client distros for node.js and web (fixes DXJ-185) - Update libp2p to 0.42.2 (fixes DXJ-26) - Add JS Client API (fixes DXJ-196, fixes DXJ-177, fixes DXJ-60) - Add Smoke test for JS Client web (fixes DXJ-253) --------- Co-authored-by: Anatoly Laskaris <github_me@nahsi.dev>
This commit is contained in:
222
packages/core/js-peer/src/compilerSupport/__test__/v3.spec.ts
Normal file
222
packages/core/js-peer/src/compilerSupport/__test__/v3.spec.ts
Normal file
@ -0,0 +1,222 @@
|
||||
import { aqua2ts, ts2aqua } from '../conversions.js';
|
||||
|
||||
const i32 = { tag: 'scalar', name: 'i32' } as const;
|
||||
|
||||
const opt_i32 = {
|
||||
tag: 'option',
|
||||
type: i32,
|
||||
} as const;
|
||||
|
||||
const array_i32 = { tag: 'array', type: i32 };
|
||||
|
||||
const array_opt_i32 = { tag: 'array', type: opt_i32 };
|
||||
|
||||
const labeledProduct = {
|
||||
tag: 'labeledProduct',
|
||||
fields: {
|
||||
a: i32,
|
||||
b: opt_i32,
|
||||
c: array_opt_i32,
|
||||
},
|
||||
};
|
||||
|
||||
const struct = {
|
||||
tag: 'struct',
|
||||
name: 'someStruct',
|
||||
fields: {
|
||||
a: i32,
|
||||
b: opt_i32,
|
||||
c: array_opt_i32,
|
||||
},
|
||||
};
|
||||
|
||||
const structs = [
|
||||
{
|
||||
aqua: {
|
||||
a: 1,
|
||||
b: [2],
|
||||
c: [[1], [2]],
|
||||
},
|
||||
|
||||
ts: {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: [1, 2],
|
||||
},
|
||||
},
|
||||
{
|
||||
aqua: {
|
||||
a: 1,
|
||||
b: [],
|
||||
c: [[], [2]],
|
||||
},
|
||||
|
||||
ts: {
|
||||
a: 1,
|
||||
b: null,
|
||||
c: [null, 2],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const labeledProduct2 = {
|
||||
tag: 'labeledProduct',
|
||||
fields: {
|
||||
x: i32,
|
||||
y: i32,
|
||||
},
|
||||
};
|
||||
|
||||
const nestedLabeledProductType = {
|
||||
tag: 'labeledProduct',
|
||||
fields: {
|
||||
a: labeledProduct2,
|
||||
b: {
|
||||
tag: 'option',
|
||||
type: labeledProduct2,
|
||||
},
|
||||
c: {
|
||||
tag: 'array',
|
||||
type: labeledProduct2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const nestedStructs = [
|
||||
{
|
||||
aqua: {
|
||||
a: {
|
||||
x: 1,
|
||||
y: 2,
|
||||
},
|
||||
b: [
|
||||
{
|
||||
x: 1,
|
||||
y: 2,
|
||||
},
|
||||
],
|
||||
c: [
|
||||
{
|
||||
x: 1,
|
||||
y: 2,
|
||||
},
|
||||
{
|
||||
x: 3,
|
||||
y: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
ts: {
|
||||
a: {
|
||||
x: 1,
|
||||
y: 2,
|
||||
},
|
||||
b: {
|
||||
x: 1,
|
||||
y: 2,
|
||||
},
|
||||
|
||||
c: [
|
||||
{
|
||||
x: 1,
|
||||
y: 2,
|
||||
},
|
||||
{
|
||||
x: 3,
|
||||
y: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
aqua: {
|
||||
a: {
|
||||
x: 1,
|
||||
y: 2,
|
||||
},
|
||||
b: [],
|
||||
c: [],
|
||||
},
|
||||
|
||||
ts: {
|
||||
a: {
|
||||
x: 1,
|
||||
y: 2,
|
||||
},
|
||||
b: null,
|
||||
c: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('Conversion from aqua to typescript', () => {
|
||||
test.each`
|
||||
aqua | ts | type
|
||||
${1} | ${1} | ${i32}
|
||||
${[]} | ${null} | ${opt_i32}
|
||||
${[1]} | ${1} | ${opt_i32}
|
||||
${[1, 2, 3]} | ${[1, 2, 3]} | ${array_i32}
|
||||
${[]} | ${[]} | ${array_i32}
|
||||
${[[1]]} | ${[1]} | ${array_opt_i32}
|
||||
${[[]]} | ${[null]} | ${array_opt_i32}
|
||||
${[[1], [2]]} | ${[1, 2]} | ${array_opt_i32}
|
||||
${[[], [2]]} | ${[null, 2]} | ${array_opt_i32}
|
||||
${structs[0].aqua} | ${structs[0].ts} | ${labeledProduct}
|
||||
${structs[1].aqua} | ${structs[1].ts} | ${labeledProduct}
|
||||
${structs[0].aqua} | ${structs[0].ts} | ${struct}
|
||||
${structs[1].aqua} | ${structs[1].ts} | ${struct}
|
||||
${nestedStructs[0].aqua} | ${nestedStructs[0].ts} | ${nestedLabeledProductType}
|
||||
${nestedStructs[1].aqua} | ${nestedStructs[1].ts} | ${nestedLabeledProductType}
|
||||
`(
|
||||
//
|
||||
'aqua: $aqua. ts: $ts. type: $type',
|
||||
async ({ aqua, ts, type }) => {
|
||||
// arrange
|
||||
|
||||
// act
|
||||
const tsFromAqua = aqua2ts(aqua, type);
|
||||
const aquaFromTs = ts2aqua(ts, type);
|
||||
|
||||
// assert
|
||||
expect(tsFromAqua).toStrictEqual(ts);
|
||||
expect(aquaFromTs).toStrictEqual(aqua);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('Conversion corner cases', () => {
|
||||
it('Should accept undefined in object entry', () => {
|
||||
// arrange
|
||||
const type = {
|
||||
tag: 'labeledProduct',
|
||||
fields: {
|
||||
x: opt_i32,
|
||||
y: opt_i32,
|
||||
},
|
||||
} as const;
|
||||
|
||||
const valueInTs = {
|
||||
x: 1,
|
||||
};
|
||||
const valueInAqua = {
|
||||
x: [1],
|
||||
y: [],
|
||||
};
|
||||
|
||||
// act
|
||||
const aqua = ts2aqua(valueInTs, type);
|
||||
const ts = aqua2ts(valueInAqua, type);
|
||||
|
||||
// assert
|
||||
expect(aqua).toStrictEqual({
|
||||
x: [1],
|
||||
y: [],
|
||||
});
|
||||
|
||||
expect(ts).toStrictEqual({
|
||||
x: 1,
|
||||
y: null,
|
||||
});
|
||||
});
|
||||
});
|
90
packages/core/js-peer/src/compilerSupport/callFunction.ts
Normal file
90
packages/core/js-peer/src/compilerSupport/callFunction.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import {
|
||||
ArrowWithoutCallbacks,
|
||||
FnConfig,
|
||||
FunctionCallDef,
|
||||
NonArrowType,
|
||||
getArgumentTypes,
|
||||
isReturnTypeVoid,
|
||||
IFluenceClient,
|
||||
} from '@fluencelabs/interfaces';
|
||||
|
||||
import {
|
||||
injectRelayService,
|
||||
registerParticleScopeService,
|
||||
responseService,
|
||||
errorHandlingService,
|
||||
ServiceDescription,
|
||||
userHandlerService,
|
||||
injectValueService,
|
||||
} from './services.js';
|
||||
|
||||
/**
|
||||
* Convenience function which does all the internal work of creating particles
|
||||
* and making necessary service registrations in order to support Aqua function calls
|
||||
*
|
||||
* @param def - function definition generated by the Aqua compiler
|
||||
* @param script - air script with function execution logic generated by the Aqua compiler
|
||||
* @param config - options to configure Aqua function execution
|
||||
* @param peer - Fluence Peer to invoke the function at
|
||||
* @param args - args in the form of JSON where each key corresponds to the name of the argument
|
||||
* @returns
|
||||
*/
|
||||
export function callFunctionImpl(
|
||||
def: FunctionCallDef,
|
||||
script: string,
|
||||
config: FnConfig,
|
||||
peer: IFluenceClient,
|
||||
args: { [key: string]: any },
|
||||
): Promise<unknown> {
|
||||
const argumentTypes = getArgumentTypes(def);
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const particle = peer.internals.createNewParticle(script, config?.ttl);
|
||||
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
||||
for (let [name, argVal] of Object.entries(args)) {
|
||||
const type = argumentTypes[name];
|
||||
let service: ServiceDescription;
|
||||
if (type.tag === 'arrow') {
|
||||
service = userHandlerService(def.names.callbackSrv, [name, type], argVal);
|
||||
} else {
|
||||
service = injectValueService(def.names.getDataSrv, name, type, argVal);
|
||||
}
|
||||
registerParticleScopeService(peer, particle, service);
|
||||
}
|
||||
|
||||
registerParticleScopeService(peer, particle, responseService(def, resolve));
|
||||
|
||||
registerParticleScopeService(peer, particle, injectRelayService(def, peer));
|
||||
|
||||
registerParticleScopeService(peer, particle, errorHandlingService(def, reject));
|
||||
|
||||
peer.internals.initiateParticle(particle, (stage: any) => {
|
||||
// If function is void, then it's completed when one of the two conditions is met:
|
||||
// 1. The particle is sent to the network (state 'sent')
|
||||
// 2. All CallRequests are executed, e.g., all variable loading and local function calls are completed (state 'localWorkDone')
|
||||
if (isReturnTypeVoid(def) && (stage.stage === 'sent' || stage.stage === 'localWorkDone')) {
|
||||
resolve(undefined);
|
||||
}
|
||||
|
||||
if (stage.stage === 'sendingError') {
|
||||
reject(`Could not send particle for ${def.functionName}: not connected (particle id: ${particle.id})`);
|
||||
}
|
||||
|
||||
if (stage.stage === 'expired') {
|
||||
reject(`Request timed out after ${particle.ttl} for ${def.functionName} (particle id: ${particle.id})`);
|
||||
}
|
||||
|
||||
if (stage.stage === 'interpreterError') {
|
||||
reject(
|
||||
`Script interpretation failed for ${def.functionName}: ${stage.errorMessage} (particle id: ${particle.id})`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
186
packages/core/js-peer/src/compilerSupport/conversions.ts
Normal file
186
packages/core/js-peer/src/compilerSupport/conversions.ts
Normal file
@ -0,0 +1,186 @@
|
||||
import { jsonify } from '../js-peer/utils.js';
|
||||
import { match } from 'ts-pattern';
|
||||
import type { ArrowType, ArrowWithoutCallbacks, NonArrowType } from '@fluencelabs/interfaces';
|
||||
import { CallServiceData } from '../interfaces/commonTypes.js';
|
||||
|
||||
/**
|
||||
* Convert value from its representation in aqua language to representation in typescript
|
||||
* @param value - value as represented in aqua
|
||||
* @param type - definition of the aqua type
|
||||
* @returns value represented in typescript
|
||||
*/
|
||||
export const aqua2ts = (value: any, type: NonArrowType): any => {
|
||||
const res = match(type)
|
||||
.with({ tag: 'nil' }, () => {
|
||||
return null;
|
||||
})
|
||||
.with({ tag: 'option' }, (opt) => {
|
||||
if (value.length === 0) {
|
||||
return null;
|
||||
} else {
|
||||
return aqua2ts(value[0], opt.type);
|
||||
}
|
||||
})
|
||||
// @ts-ignore
|
||||
.with({ tag: 'scalar' }, { tag: 'bottomType' }, { tag: 'topType' }, () => {
|
||||
return value;
|
||||
})
|
||||
.with({ tag: 'array' }, (arr) => {
|
||||
return value.map((y: any) => aqua2ts(y, arr.type));
|
||||
})
|
||||
.with({ tag: 'struct' }, (x) => {
|
||||
return Object.entries(x.fields).reduce((agg, [key, type]) => {
|
||||
const val = aqua2ts(value[key], type);
|
||||
return { ...agg, [key]: val };
|
||||
}, {});
|
||||
})
|
||||
.with({ tag: 'labeledProduct' }, (x) => {
|
||||
return Object.entries(x.fields).reduce((agg, [key, type]) => {
|
||||
const val = aqua2ts(value[key], type);
|
||||
return { ...agg, [key]: val };
|
||||
}, {});
|
||||
})
|
||||
.with({ tag: 'unlabeledProduct' }, (x) => {
|
||||
return x.items.map((type, index) => {
|
||||
return aqua2ts(value[index], type);
|
||||
});
|
||||
})
|
||||
// uncomment to check that every pattern in matched
|
||||
// .exhaustive();
|
||||
.otherwise(() => {
|
||||
throw new Error('Unexpected tag: ' + jsonify(type));
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert call service arguments list from their aqua representation to representation in typescript
|
||||
* @param req - call service data
|
||||
* @param arrow - aqua type definition
|
||||
* @returns arguments in typescript representation
|
||||
*/
|
||||
export const aquaArgs2Ts = (req: CallServiceData, arrow: ArrowWithoutCallbacks) => {
|
||||
const argTypes = match(arrow.domain)
|
||||
.with({ tag: 'labeledProduct' }, (x) => {
|
||||
return Object.values(x.fields);
|
||||
})
|
||||
.with({ tag: 'unlabeledProduct' }, (x) => {
|
||||
return x.items;
|
||||
})
|
||||
.with({ tag: 'nil' }, (x) => {
|
||||
return [];
|
||||
})
|
||||
// uncomment to check that every pattern in matched
|
||||
// .exhaustive()
|
||||
.otherwise(() => {
|
||||
throw new Error('Unexpected tag: ' + jsonify(arrow.domain));
|
||||
});
|
||||
|
||||
if (req.args.length !== argTypes.length) {
|
||||
throw new Error(`incorrect number of arguments, expected: ${argTypes.length}, got: ${req.args.length}`);
|
||||
}
|
||||
|
||||
return req.args.map((arg, index) => {
|
||||
return aqua2ts(arg, argTypes[index]);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert value from its typescript representation to representation in aqua
|
||||
* @param value - the value as represented in typescript
|
||||
* @param type - definition of the aqua type
|
||||
* @returns value represented in aqua
|
||||
*/
|
||||
export const ts2aqua = (value: any, type: NonArrowType): any => {
|
||||
const res = match(type)
|
||||
.with({ tag: 'nil' }, () => {
|
||||
return null;
|
||||
})
|
||||
.with({ tag: 'option' }, (opt) => {
|
||||
if (value === null || value === undefined) {
|
||||
return [];
|
||||
} else {
|
||||
return [ts2aqua(value, opt.type)];
|
||||
}
|
||||
})
|
||||
// @ts-ignore
|
||||
.with({ tag: 'scalar' }, { tag: 'bottomType' }, { tag: 'topType' }, () => {
|
||||
return value;
|
||||
})
|
||||
.with({ tag: 'array' }, (arr) => {
|
||||
return value.map((y: any) => ts2aqua(y, arr.type));
|
||||
})
|
||||
.with({ tag: 'struct' }, (x) => {
|
||||
return Object.entries(x.fields).reduce((agg, [key, type]) => {
|
||||
const val = ts2aqua(value[key], type);
|
||||
return { ...agg, [key]: val };
|
||||
}, {});
|
||||
})
|
||||
.with({ tag: 'labeledProduct' }, (x) => {
|
||||
return Object.entries(x.fields).reduce((agg, [key, type]) => {
|
||||
const val = ts2aqua(value[key], type);
|
||||
return { ...agg, [key]: val };
|
||||
}, {});
|
||||
})
|
||||
.with({ tag: 'unlabeledProduct' }, (x) => {
|
||||
return x.items.map((type, index) => {
|
||||
return ts2aqua(value[index], type);
|
||||
});
|
||||
})
|
||||
// uncomment to check that every pattern in matched
|
||||
// .exhaustive()
|
||||
.otherwise(() => {
|
||||
throw new Error('Unexpected tag: ' + jsonify(type));
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert return type of the service from it's typescript representation to representation in aqua
|
||||
* @param returnValue - the value as represented in typescript
|
||||
* @param arrowType - the arrow type which describes the service
|
||||
* @returns - value represented in aqua
|
||||
*/
|
||||
export const returnType2Aqua = (returnValue: any, arrowType: ArrowType<NonArrowType>) => {
|
||||
if (arrowType.codomain.tag === 'nil') {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (arrowType.codomain.items.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (arrowType.codomain.items.length === 1) {
|
||||
return ts2aqua(returnValue, arrowType.codomain.items[0]);
|
||||
}
|
||||
|
||||
return arrowType.codomain.items.map((type, index) => {
|
||||
return ts2aqua(returnValue[index], type);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts response value from aqua its representation to representation in typescript
|
||||
* @param req - call service data
|
||||
* @param arrow - aqua type definition
|
||||
* @returns response value in typescript representation
|
||||
*/
|
||||
export const responseServiceValue2ts = (req: CallServiceData, arrow: ArrowType<any>) => {
|
||||
return match(arrow.codomain)
|
||||
.with({ tag: 'nil' }, () => {
|
||||
return undefined;
|
||||
})
|
||||
.with({ tag: 'unlabeledProduct' }, (x) => {
|
||||
if (x.items.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (x.items.length === 1) {
|
||||
return aqua2ts(req.args[0], x.items[0]);
|
||||
}
|
||||
|
||||
return req.args.map((y, index) => aqua2ts(y, x.items[index]));
|
||||
})
|
||||
.exhaustive();
|
||||
};
|
46
packages/core/js-peer/src/compilerSupport/registerService.ts
Normal file
46
packages/core/js-peer/src/compilerSupport/registerService.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import type { IFluenceClient, ServiceDef } from '@fluencelabs/interfaces';
|
||||
import { registerGlobalService, userHandlerService } from './services.js';
|
||||
|
||||
export const registerServiceImpl = (
|
||||
peer: IFluenceClient,
|
||||
def: ServiceDef,
|
||||
serviceId: string | undefined,
|
||||
service: any,
|
||||
) => {
|
||||
// TODO: TBH service registration is just putting some stuff into a hashmap
|
||||
// there should not be such a check at all
|
||||
if (!peer.getStatus().isInitialized) {
|
||||
throw new Error(
|
||||
'Could not register the service because the peer is not initialized. Are you passing the wrong peer to the register function?',
|
||||
);
|
||||
}
|
||||
|
||||
// Checking for missing keys
|
||||
const requiredKeys = def.functions.tag === 'nil' ? [] : Object.keys(def.functions.fields);
|
||||
const incorrectServiceDefinitions = requiredKeys.filter((f) => !(f in service));
|
||||
if (!!incorrectServiceDefinitions.length) {
|
||||
throw new Error(
|
||||
`Error registering service ${serviceId}: missing functions: ` +
|
||||
incorrectServiceDefinitions.map((d) => "'" + d + "'").join(', '),
|
||||
);
|
||||
}
|
||||
|
||||
if (!serviceId) {
|
||||
serviceId = def.defaultServiceId;
|
||||
}
|
||||
|
||||
if (!serviceId) {
|
||||
throw new Error('Service ID must be specified');
|
||||
}
|
||||
|
||||
const singleFunctions = def.functions.tag === 'nil' ? [] : Object.entries(def.functions.fields);
|
||||
for (let singleFunction of singleFunctions) {
|
||||
let [name, type] = singleFunction;
|
||||
// The function has type of (arg1, arg2, arg3, ... , callParams) => CallServiceResultType | void
|
||||
// Account for the fact that user service might be defined as a class - .bind(...)
|
||||
const userDefinedHandler = service[name].bind(service);
|
||||
|
||||
const serviceDescription = userHandlerService(serviceId, singleFunction, userDefinedHandler);
|
||||
registerGlobalService(peer, serviceDescription);
|
||||
}
|
||||
};
|
177
packages/core/js-peer/src/compilerSupport/services.ts
Normal file
177
packages/core/js-peer/src/compilerSupport/services.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import { SecurityTetraplet } from '@fluencelabs/avm';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { Particle } from '../js-peer/Particle.js';
|
||||
import { CallServiceData, GenericCallServiceHandler, ResultCodes } from '../interfaces/commonTypes.js';
|
||||
|
||||
import { aquaArgs2Ts, responseServiceValue2ts, returnType2Aqua, ts2aqua } from './conversions.js';
|
||||
import {
|
||||
IFluenceClient,
|
||||
CallParams,
|
||||
ArrowWithoutCallbacks,
|
||||
FunctionCallConstants,
|
||||
FunctionCallDef,
|
||||
NonArrowType,
|
||||
} from '@fluencelabs/interfaces';
|
||||
|
||||
export interface ServiceDescription {
|
||||
serviceId: string;
|
||||
fnName: string;
|
||||
handler: GenericCallServiceHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a service which injects relay's peer id into aqua space
|
||||
*/
|
||||
export const injectRelayService = (def: FunctionCallDef, peer: IFluenceClient) => {
|
||||
return {
|
||||
serviceId: def.names.getDataSrv,
|
||||
fnName: def.names.relay,
|
||||
handler: () => {
|
||||
return {
|
||||
retCode: ResultCodes.success,
|
||||
result: peer.getStatus().relayPeerId,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a service which injects plain value into aqua space
|
||||
*/
|
||||
export const injectValueService = (serviceId: string, fnName: string, valueType: NonArrowType, value: any) => {
|
||||
return {
|
||||
serviceId: serviceId,
|
||||
fnName: fnName,
|
||||
handler: () => {
|
||||
return {
|
||||
retCode: ResultCodes.success,
|
||||
result: ts2aqua(value, valueType),
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a service which is used to return value from aqua function into typescript space
|
||||
*/
|
||||
export const responseService = (def: FunctionCallDef, resolveCallback: Function) => {
|
||||
return {
|
||||
serviceId: def.names.responseSrv,
|
||||
fnName: def.names.responseFnName,
|
||||
handler: (req: CallServiceData) => {
|
||||
const userFunctionReturn = responseServiceValue2ts(req, def.arrow);
|
||||
|
||||
setTimeout(() => {
|
||||
resolveCallback(userFunctionReturn);
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
retCode: ResultCodes.success,
|
||||
result: {},
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a service which is used to return errors from aqua function into typescript space
|
||||
*/
|
||||
export const errorHandlingService = (def: FunctionCallDef, rejectCallback: Function) => {
|
||||
return {
|
||||
serviceId: def.names.errorHandlingSrv,
|
||||
fnName: def.names.errorFnName,
|
||||
handler: (req: CallServiceData) => {
|
||||
const [err, _] = req.args;
|
||||
setTimeout(() => {
|
||||
rejectCallback(err);
|
||||
}, 0);
|
||||
return {
|
||||
retCode: ResultCodes.success,
|
||||
result: {},
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a service for user-defined service function handler
|
||||
*/
|
||||
export const userHandlerService = (
|
||||
serviceId: string,
|
||||
arrowType: [string, ArrowWithoutCallbacks],
|
||||
userHandler: (...args: Array<unknown>) => Promise<unknown>,
|
||||
) => {
|
||||
const [fnName, type] = arrowType;
|
||||
return {
|
||||
serviceId,
|
||||
fnName,
|
||||
handler: async (req: CallServiceData) => {
|
||||
const args = [...aquaArgs2Ts(req, type), extractCallParams(req, type)];
|
||||
const rawResult = await userHandler.apply(null, args);
|
||||
const result = returnType2Aqua(rawResult, type);
|
||||
|
||||
return {
|
||||
retCode: ResultCodes.success,
|
||||
result: result,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts argument of aqua function to a corresponding service.
|
||||
* For arguments of non-arrow types the resulting service injects the argument into aqua space.
|
||||
* For arguments of arrow types the resulting service calls the corresponding function.
|
||||
*/
|
||||
export const argToServiceDef = (
|
||||
arg: any,
|
||||
argName: string,
|
||||
argType: NonArrowType | ArrowWithoutCallbacks,
|
||||
names: FunctionCallConstants,
|
||||
): ServiceDescription => {
|
||||
if (argType.tag === 'arrow') {
|
||||
return userHandlerService(names.callbackSrv, [argName, argType], arg);
|
||||
} else {
|
||||
return injectValueService(names.getDataSrv, argName, arg, argType);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts call params from from call service data according to aqua type definition
|
||||
*/
|
||||
const extractCallParams = (req: CallServiceData, arrow: ArrowWithoutCallbacks): CallParams<any> => {
|
||||
const names = match(arrow.domain)
|
||||
.with({ tag: 'nil' }, () => {
|
||||
return [] as string[];
|
||||
})
|
||||
.with({ tag: 'labeledProduct' }, (x) => {
|
||||
return Object.keys(x.fields);
|
||||
})
|
||||
.with({ tag: 'unlabeledProduct' }, (x) => {
|
||||
return x.items.map((_, index) => 'arg' + index);
|
||||
})
|
||||
.exhaustive();
|
||||
|
||||
const tetraplets: Record<string, SecurityTetraplet[]> = {};
|
||||
for (let i = 0; i < req.args.length; i++) {
|
||||
if (names[i]) {
|
||||
tetraplets[names[i]] = req.tetraplets[i];
|
||||
}
|
||||
}
|
||||
|
||||
const callParams = {
|
||||
...req.particleContext,
|
||||
tetraplets,
|
||||
};
|
||||
|
||||
return callParams;
|
||||
};
|
||||
|
||||
export const registerParticleScopeService = (peer: IFluenceClient, particle: Particle, service: ServiceDescription) => {
|
||||
peer.internals.regHandler.forParticle(particle.id, service.serviceId, service.fnName, service.handler);
|
||||
};
|
||||
|
||||
export const registerGlobalService = (peer: IFluenceClient, service: ServiceDescription) => {
|
||||
peer.internals.regHandler.common(service.serviceId, service.fnName, service.handler);
|
||||
};
|
174
packages/core/js-peer/src/connection/index.ts
Normal file
174
packages/core/js-peer/src/connection/index.ts
Normal file
@ -0,0 +1,174 @@
|
||||
/*
|
||||
* 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 { PeerIdB58 } from '@fluencelabs/interfaces';
|
||||
import { FluenceConnection, ParticleHandler } from '../interfaces/index.js';
|
||||
import { pipe } from 'it-pipe';
|
||||
import { encode, decode } from 'it-length-prefixed';
|
||||
import type { PeerId } from '@libp2p/interface-peer-id';
|
||||
import { createLibp2p, Libp2p } from 'libp2p';
|
||||
|
||||
import { noise } from '@chainsafe/libp2p-noise';
|
||||
import { mplex } from '@libp2p/mplex';
|
||||
import { webSockets } from '@libp2p/websockets';
|
||||
import { all } from '@libp2p/websockets/filters';
|
||||
import { multiaddr } from '@multiformats/multiaddr';
|
||||
import type { MultiaddrInput, Multiaddr } from '@multiformats/multiaddr';
|
||||
import type { Connection } from '@libp2p/interface-connection';
|
||||
|
||||
import map from 'it-map';
|
||||
import { fromString } from 'uint8arrays/from-string';
|
||||
import { toString } from 'uint8arrays/to-string';
|
||||
|
||||
import log from 'loglevel';
|
||||
|
||||
export const PROTOCOL_NAME = '/fluence/particle/2.0.0';
|
||||
|
||||
/**
|
||||
* Options to configure fluence connection
|
||||
*/
|
||||
export interface FluenceConnectionOptions {
|
||||
/**
|
||||
* Peer id of the Fluence Peer
|
||||
*/
|
||||
peerId: PeerId;
|
||||
|
||||
/**
|
||||
* Multiaddress of the relay to make connection to
|
||||
*/
|
||||
relayAddress: MultiaddrInput;
|
||||
|
||||
/**
|
||||
* The dialing timeout in milliseconds
|
||||
*/
|
||||
dialTimeoutMs?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation for JS peers which connects to Fluence through relay node
|
||||
*/
|
||||
export class RelayConnection extends FluenceConnection {
|
||||
constructor(
|
||||
public peerId: PeerIdB58,
|
||||
private _lib2p2Peer: Libp2p,
|
||||
private _relayAddress: Multiaddr,
|
||||
public readonly relayPeerId: PeerIdB58,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private _connection?: Connection;
|
||||
|
||||
static async createConnection(options: FluenceConnectionOptions): Promise<RelayConnection> {
|
||||
const lib2p2Peer = await createLibp2p({
|
||||
peerId: options.peerId,
|
||||
transports: [
|
||||
webSockets({
|
||||
filter: all,
|
||||
}),
|
||||
],
|
||||
streamMuxers: [mplex()],
|
||||
connectionEncryption: [noise()],
|
||||
});
|
||||
|
||||
const relayMultiaddr = multiaddr(options.relayAddress);
|
||||
const relayPeerId = relayMultiaddr.getPeerId();
|
||||
if (relayPeerId === null) {
|
||||
throw new Error('Specified multiaddr is invalid or missing peer id: ' + options.relayAddress);
|
||||
}
|
||||
|
||||
return new RelayConnection(
|
||||
// force new line
|
||||
options.peerId.toString(),
|
||||
lib2p2Peer,
|
||||
relayMultiaddr,
|
||||
relayPeerId,
|
||||
);
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
await this._lib2p2Peer.unhandle(PROTOCOL_NAME);
|
||||
await this._lib2p2Peer.stop();
|
||||
}
|
||||
|
||||
async sendParticle(nextPeerIds: PeerIdB58[], particle: string): Promise<void> {
|
||||
if (nextPeerIds.length !== 1 && nextPeerIds[0] !== this.relayPeerId) {
|
||||
throw new Error(
|
||||
`Relay connection only accepts peer id of the connected relay. Got: ${JSON.stringify(
|
||||
nextPeerIds,
|
||||
)} instead.`,
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
TODO:: find out why this doesn't work and a new connection has to be established each time
|
||||
if (this._connection.streams.length !== 1) {
|
||||
throw new Error('Incorrect number of streams in FluenceConnection');
|
||||
}
|
||||
|
||||
const sink = this._connection.streams[0].sink;
|
||||
*/
|
||||
|
||||
const stream = await this._lib2p2Peer.dialProtocol(this._relayAddress, PROTOCOL_NAME);
|
||||
const sink = stream.sink;
|
||||
|
||||
pipe(
|
||||
[fromString(particle)],
|
||||
// @ts-ignore
|
||||
encode(),
|
||||
sink,
|
||||
);
|
||||
}
|
||||
|
||||
async connect(onIncomingParticle: ParticleHandler) {
|
||||
await this._lib2p2Peer.start();
|
||||
|
||||
this._lib2p2Peer.handle([PROTOCOL_NAME], async ({ connection, stream }) => {
|
||||
pipe(
|
||||
stream.source,
|
||||
// @ts-ignore
|
||||
decode(),
|
||||
// @ts-ignore
|
||||
(source) => map(source, (buf) => toString(buf.subarray())),
|
||||
async (source) => {
|
||||
try {
|
||||
for await (const msg of source) {
|
||||
try {
|
||||
onIncomingParticle(msg);
|
||||
} catch (e) {
|
||||
log.error('error on handling a new incoming message: ' + e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log.debug('connection closed: ' + e);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
log.debug(`dialing to the node with client's address: ` + this._lib2p2Peer.peerId.toString());
|
||||
|
||||
try {
|
||||
this._connection = await this._lib2p2Peer.dial(this._relayAddress);
|
||||
} catch (e: any) {
|
||||
if (e.name === 'AggregateError' && e._errors?.length === 1) {
|
||||
const error = e._errors[0];
|
||||
throw new Error(`Error dialing node ${this._relayAddress}:\n${error.code}\n${error.message}`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
112
packages/core/js-peer/src/interfaces/commonTypes.ts
Normal file
112
packages/core/js-peer/src/interfaces/commonTypes.ts
Normal file
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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 type { PeerIdB58 } from '@fluencelabs/interfaces';
|
||||
import type { SecurityTetraplet } from '@fluencelabs/avm';
|
||||
|
||||
export enum ResultCodes {
|
||||
success = 0,
|
||||
error = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Particle context. Contains additional information about particle which triggered `call` air instruction from AVM
|
||||
*/
|
||||
export interface ParticleContext {
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the information passed from AVM when a `call` air instruction is executed on the local peer
|
||||
*/
|
||||
export interface CallServiceData {
|
||||
/**
|
||||
* Service ID as specified in `call` air instruction
|
||||
*/
|
||||
serviceId: string;
|
||||
|
||||
/**
|
||||
* Function name as specified in `call` air instruction
|
||||
*/
|
||||
fnName: string;
|
||||
|
||||
/**
|
||||
* Arguments as specified in `call` air instruction
|
||||
*/
|
||||
args: any[];
|
||||
|
||||
/**
|
||||
* Security Tetraplets received from AVM
|
||||
*/
|
||||
tetraplets: SecurityTetraplet[][];
|
||||
|
||||
/**
|
||||
* Particle context, @see {@link ParticleContext}
|
||||
*/
|
||||
particleContext: ParticleContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for all the possible objects that can be returned to the AVM
|
||||
*/
|
||||
export type CallServiceResultType = JSONValue;
|
||||
|
||||
/**
|
||||
* Generic call service handler
|
||||
*/
|
||||
export type GenericCallServiceHandler = (req: CallServiceData) => CallServiceResult | Promise<CallServiceResult>;
|
||||
|
||||
/**
|
||||
* Represents the result of the `call` air instruction to be returned into AVM
|
||||
*/
|
||||
export interface CallServiceResult {
|
||||
/**
|
||||
* Return code to be returned to AVM
|
||||
*/
|
||||
retCode: ResultCodes;
|
||||
|
||||
/**
|
||||
* Result object to be returned to AVM
|
||||
*/
|
||||
result: CallServiceResultType;
|
||||
}
|
||||
|
||||
export type JSONValue = string | number | boolean | null | { [x: string]: JSONValue } | Array<JSONValue>;
|
||||
export type JSONArray = Array<JSONValue>;
|
||||
export type JSONObject = { [x: string]: JSONValue };
|
90
packages/core/js-peer/src/interfaces/index.ts
Normal file
90
packages/core/js-peer/src/interfaces/index.ts
Normal file
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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 type { PeerIdB58 } from '@fluencelabs/interfaces';
|
||||
import type { JSONArray, JSONObject, LogLevel } from '@fluencelabs/marine-js/dist/types';
|
||||
import type { RunParameters, CallResultsArray, InterpreterResult } from '@fluencelabs/avm';
|
||||
import type { WorkerImplementation } from 'threads/dist/types/master';
|
||||
|
||||
export type ParticleHandler = (particle: string) => void;
|
||||
|
||||
/**
|
||||
* Base class for connectivity layer to Fluence Network
|
||||
*/
|
||||
export abstract class FluenceConnection {
|
||||
abstract readonly relayPeerId: PeerIdB58 | null;
|
||||
abstract connect(onIncomingParticle: ParticleHandler): Promise<void>;
|
||||
abstract disconnect(): Promise<void>;
|
||||
abstract sendParticle(nextPeerIds: PeerIdB58[], particle: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IMarine extends IModule {
|
||||
createService(serviceModule: SharedArrayBuffer | Buffer, serviceId: string, logLevel?: LogLevel): Promise<void>;
|
||||
|
||||
callService(
|
||||
serviceId: string,
|
||||
functionName: string,
|
||||
args: JSONArray | JSONObject,
|
||||
callParams: any,
|
||||
): Promise<unknown>;
|
||||
}
|
||||
|
||||
export interface IAvmRunner extends IModule {
|
||||
run(
|
||||
runParams: RunParameters,
|
||||
air: string,
|
||||
prevData: Uint8Array,
|
||||
data: Uint8Array,
|
||||
callResults: CallResultsArray,
|
||||
): Promise<InterpreterResult | Error>;
|
||||
}
|
||||
|
||||
export interface IModule {
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IValueLoader<T> {
|
||||
getValue(): T;
|
||||
}
|
||||
|
||||
export interface IWasmLoader extends IValueLoader<SharedArrayBuffer | Buffer>, IModule {}
|
||||
|
||||
export interface IWorkerLoader extends IValueLoader<WorkerImplementation>, IModule {}
|
||||
|
||||
export class LazyLoader<T> implements IModule, IValueLoader<T> {
|
||||
private value: T | null = null;
|
||||
|
||||
constructor(private loadValue: () => Promise<T> | T) {}
|
||||
|
||||
getValue(): T {
|
||||
if (this.value == null) {
|
||||
throw new Error('Value has not been loaded. Call `start` method to load the value.');
|
||||
}
|
||||
|
||||
return this.value;
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.value !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.value = await this.loadValue();
|
||||
}
|
||||
|
||||
async stop() {}
|
||||
}
|
729
packages/core/js-peer/src/js-peer/FluencePeer.ts
Normal file
729
packages/core/js-peer/src/js-peer/FluencePeer.ts
Normal file
@ -0,0 +1,729 @@
|
||||
/*
|
||||
* Copyright 2021 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import 'buffer';
|
||||
|
||||
import { RelayConnection } from '../connection/index.js';
|
||||
import { FluenceConnection, IAvmRunner, IMarine } from '../interfaces/index.js';
|
||||
import { fromOpts, KeyPair } from '../keypair/index.js';
|
||||
import {
|
||||
CallServiceData,
|
||||
CallServiceResult,
|
||||
GenericCallServiceHandler,
|
||||
ResultCodes,
|
||||
} from '../interfaces/commonTypes.js';
|
||||
import type {
|
||||
PeerIdB58,
|
||||
IFluenceClient,
|
||||
PeerStatus,
|
||||
CallFunctionArgs,
|
||||
RegisterServiceArgs,
|
||||
ClientOptions,
|
||||
KeyPairOptions,
|
||||
RelayOptions,
|
||||
} from '@fluencelabs/interfaces/dist/fluenceClient';
|
||||
import { Particle, ParticleExecutionStage, ParticleQueueItem } from './Particle.js';
|
||||
import { dataToString, jsonify, isString, ServiceError } from './utils.js';
|
||||
import { concatMap, filter, pipe, Subject, tap } from 'rxjs';
|
||||
import log from 'loglevel';
|
||||
import { builtInServices } from './builtins/common.js';
|
||||
import { defaultSigGuard, Sig } from './builtins/Sig.js';
|
||||
import { registerSig } from './_aqua/services.js';
|
||||
import { registerSrv } from './_aqua/single-module-srv.js';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import { JSONValue } from '@fluencelabs/avm';
|
||||
import { LogLevel } from '@fluencelabs/marine-js/dist/types';
|
||||
import { NodeUtils, Srv } from './builtins/SingleModuleSrv.js';
|
||||
import { registerNodeUtils } from './_aqua/node-utils.js';
|
||||
import type { MultiaddrInput } from '@multiformats/multiaddr';
|
||||
import { callFunctionImpl } from '../compilerSupport/callFunction.js';
|
||||
import { registerServiceImpl } from '../compilerSupport/registerService.js';
|
||||
|
||||
const DEFAULT_TTL = 7000;
|
||||
|
||||
export type PeerConfig = ClientOptions;
|
||||
|
||||
/**
|
||||
* This class implements the Fluence protocol for javascript-based environments.
|
||||
* It provides all the necessary features to communicate with Fluence network
|
||||
*/
|
||||
export class FluencePeer implements IFluenceClient {
|
||||
constructor(private marine: IMarine, private avmRunner: IAvmRunner) {}
|
||||
|
||||
/**
|
||||
* Internal contract to cast unknown objects to IFluenceClient.
|
||||
* If an unknown object has this property then we assume it is in fact a Peer and it implements IFluenceClient
|
||||
* Check against this variable MUST NOT be coupled with any `FluencePeer` because otherwise it might get bundled
|
||||
* brining a lot of unnecessary stuff alongside with it
|
||||
*/
|
||||
__isFluenceAwesome = true;
|
||||
|
||||
/**
|
||||
* Get the peer's status
|
||||
*/
|
||||
getStatus(): PeerStatus {
|
||||
// TODO:: use explicit mechanism for peer's state
|
||||
if (this._keyPair === undefined) {
|
||||
return {
|
||||
isInitialized: false,
|
||||
peerId: null,
|
||||
isConnected: false,
|
||||
relayPeerId: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.connection === null) {
|
||||
return {
|
||||
isInitialized: true,
|
||||
peerId: this._keyPair.getPeerId(),
|
||||
isConnected: false,
|
||||
relayPeerId: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.connection.relayPeerId === null) {
|
||||
return {
|
||||
isInitialized: true,
|
||||
peerId: this._keyPair.getPeerId(),
|
||||
isConnected: true,
|
||||
isDirect: true,
|
||||
relayPeerId: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isInitialized: true,
|
||||
peerId: this._keyPair.getPeerId(),
|
||||
isConnected: true,
|
||||
relayPeerId: this.connection.relayPeerId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return peers SK
|
||||
*/
|
||||
getSk(): Uint8Array {
|
||||
if (!this._keyPair) {
|
||||
throw new Error("Can't get key pair: peer is not initialized");
|
||||
}
|
||||
|
||||
return this._keyPair.toEd25519PrivateKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the peer: starts the Aqua VM, initializes the default call service handlers
|
||||
* and (optionally) connect to the Fluence network
|
||||
* @param config - object specifying peer configuration
|
||||
*/
|
||||
async start(config: PeerConfig = {}): Promise<void> {
|
||||
const keyPair = await makeKeyPair(config.keyPair);
|
||||
await this.init(config, keyPair);
|
||||
|
||||
const conn = await configToConnection(keyPair, config?.relay, config?.connectionOptions?.dialTimeoutMs);
|
||||
|
||||
if (conn !== null) {
|
||||
await this.connect(conn);
|
||||
}
|
||||
}
|
||||
|
||||
getServices() {
|
||||
if (this._classServices === undefined) {
|
||||
throw new Error(`Can't get services: peer is not initialized`);
|
||||
}
|
||||
return {
|
||||
...this._classServices,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers marine service within the Fluence peer from wasm file.
|
||||
* Following helper functions can be used to load wasm files:
|
||||
* * loadWasmFromFileSystem
|
||||
* * loadWasmFromNpmPackage
|
||||
* * loadWasmFromServer
|
||||
* @param wasm - buffer with the wasm file for service
|
||||
* @param serviceId - the service id by which the service can be accessed in aqua
|
||||
*/
|
||||
async registerMarineService(wasm: SharedArrayBuffer | Buffer, serviceId: string): Promise<void> {
|
||||
if (!this.marine) {
|
||||
throw new Error("Can't register marine service: peer is not initialized");
|
||||
}
|
||||
if (this._containsService(serviceId)) {
|
||||
throw new Error(`Service with '${serviceId}' id already exists`);
|
||||
}
|
||||
|
||||
await this.marine.createService(wasm, serviceId, this._marineLogLevel);
|
||||
this._marineServices.add(serviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified marine service from the Fluence peer
|
||||
* @param serviceId - the service id to remove
|
||||
*/
|
||||
removeMarineService(serviceId: string): void {
|
||||
this._marineServices.delete(serviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Un-initializes the peer: stops all the underlying workflows, stops the Aqua VM
|
||||
* and disconnects from the Fluence network
|
||||
*/
|
||||
async stop() {
|
||||
this._keyPair = undefined; // This will set peer to non-initialized state and stop particle processing
|
||||
this._stopParticleProcessing();
|
||||
await this.disconnect();
|
||||
await this.marine.stop();
|
||||
await this.avmRunner.stop();
|
||||
this._classServices = undefined;
|
||||
|
||||
this._particleSpecificHandlers.clear();
|
||||
this._commonHandlers.clear();
|
||||
this._marineServices.clear();
|
||||
}
|
||||
|
||||
// internal api
|
||||
get compilerSupport() {
|
||||
return {
|
||||
callFunction: (args: CallFunctionArgs): Promise<unknown> => {
|
||||
return callFunctionImpl(args.def, args.script, args.config, this, args.args);
|
||||
},
|
||||
registerService: (args: RegisterServiceArgs): void => {
|
||||
return registerServiceImpl(this, args.def, args.serviceId, args.service);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private Is not intended to be used manually. Subject to change
|
||||
*/
|
||||
get internals() {
|
||||
return {
|
||||
parseAst: async (air: string): Promise<{ success: boolean; data: any }> => {
|
||||
const status = this.getStatus();
|
||||
|
||||
if (!status.isInitialized) {
|
||||
new Error("Can't use avm: peer is not initialized");
|
||||
}
|
||||
|
||||
const res = await this.marine.callService('avm', 'ast', [air], undefined);
|
||||
if (!isString(res)) {
|
||||
throw new Error(`Call to avm:ast expected to return string. Actual return: ${res}`);
|
||||
}
|
||||
|
||||
try {
|
||||
if (res.startsWith('error')) {
|
||||
return {
|
||||
success: false,
|
||||
data: res,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
data: JSON.parse(res),
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error('Failed to call avm. Result: ' + res + '. Error: ' + err);
|
||||
}
|
||||
},
|
||||
createNewParticle: (script: string, ttl: number = this._defaultTTL) => {
|
||||
const status = this.getStatus();
|
||||
|
||||
if (!status.isInitialized) {
|
||||
return new Error("Can't create new particle: peer is not initialized");
|
||||
}
|
||||
|
||||
return Particle.createNew(script, ttl, status.peerId);
|
||||
},
|
||||
/**
|
||||
* Initiates a new particle execution starting from local peer
|
||||
* @param particle - particle to start execution of
|
||||
*/
|
||||
initiateParticle: (particle: Particle, onStageChange: (stage: ParticleExecutionStage) => void): void => {
|
||||
const status = this.getStatus();
|
||||
if (!status.isInitialized) {
|
||||
throw new Error('Cannot initiate new particle: peer is not initialized');
|
||||
}
|
||||
|
||||
if (this._printParticleId) {
|
||||
console.log('Particle id: ', particle.id);
|
||||
}
|
||||
|
||||
if (particle.initPeerId === undefined) {
|
||||
particle.initPeerId = status.peerId;
|
||||
}
|
||||
|
||||
if (particle.ttl === undefined) {
|
||||
particle.ttl = this._defaultTTL;
|
||||
}
|
||||
|
||||
this._incomingParticles.next({
|
||||
particle: particle,
|
||||
onStageChange: onStageChange,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Register Call Service handler functions
|
||||
*/
|
||||
regHandler: {
|
||||
/**
|
||||
* Register handler for all particles
|
||||
*/
|
||||
common: (
|
||||
// force new line
|
||||
serviceId: string,
|
||||
fnName: string,
|
||||
handler: GenericCallServiceHandler,
|
||||
) => {
|
||||
this._commonHandlers.set(serviceFnKey(serviceId, fnName), handler);
|
||||
},
|
||||
/**
|
||||
* Register handler which will be called only for particle with the specific id
|
||||
*/
|
||||
forParticle: (
|
||||
particleId: string,
|
||||
serviceId: string,
|
||||
fnName: string,
|
||||
handler: GenericCallServiceHandler,
|
||||
) => {
|
||||
let psh = this._particleSpecificHandlers.get(particleId);
|
||||
if (psh === undefined) {
|
||||
psh = new Map<string, GenericCallServiceHandler>();
|
||||
this._particleSpecificHandlers.set(particleId, psh);
|
||||
}
|
||||
|
||||
psh.set(serviceFnKey(serviceId, fnName), handler);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private Subject to change. Do not use this method directly
|
||||
*/
|
||||
async init(config: Omit<PeerConfig, 'keyPair'>, keyPair: KeyPair) {
|
||||
this._keyPair = keyPair;
|
||||
|
||||
const peerId = this._keyPair.getPeerId();
|
||||
|
||||
if (config?.debug?.printParticleId) {
|
||||
this._printParticleId = true;
|
||||
}
|
||||
|
||||
this._defaultTTL = config?.defaultTtlMs ?? DEFAULT_TTL;
|
||||
|
||||
if (config?.debug?.marineLogLevel) {
|
||||
this._marineLogLevel = config.debug.marineLogLevel;
|
||||
}
|
||||
|
||||
await this.marine.start();
|
||||
await this.avmRunner.start();
|
||||
|
||||
registerDefaultServices(this);
|
||||
|
||||
this._classServices = {
|
||||
sig: new Sig(this._keyPair),
|
||||
srv: new Srv(this),
|
||||
};
|
||||
this._classServices.sig.securityGuard = defaultSigGuard(peerId);
|
||||
registerSig(this, 'sig', this._classServices.sig);
|
||||
registerSig(this, peerId, this._classServices.sig);
|
||||
|
||||
registerSrv(this, 'single_module_srv', this._classServices.srv);
|
||||
registerNodeUtils(this, 'node_utils', new NodeUtils(this));
|
||||
|
||||
this._startParticleProcessing();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private Subject to change. Do not use this method directly
|
||||
*/
|
||||
async connect(connection: FluenceConnection): Promise<void> {
|
||||
if (this.connection) {
|
||||
await this.connection.disconnect();
|
||||
}
|
||||
|
||||
this.connection = connection;
|
||||
await this.connection.connect(this._onIncomingParticle.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* @private Subject to change. Do not use this method directly
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
await this.connection?.disconnect();
|
||||
}
|
||||
|
||||
// private
|
||||
|
||||
// Queues for incoming and outgoing particles
|
||||
|
||||
private _incomingParticles = new Subject<ParticleQueueItem>();
|
||||
private _outgoingParticles = new Subject<ParticleQueueItem & { nextPeerIds: PeerIdB58[] }>();
|
||||
|
||||
// Call service handler
|
||||
|
||||
private _marineServices = new Set<string>();
|
||||
private _marineLogLevel?: LogLevel;
|
||||
private _particleSpecificHandlers = new Map<string, Map<string, GenericCallServiceHandler>>();
|
||||
private _commonHandlers = new Map<string, GenericCallServiceHandler>();
|
||||
|
||||
private _classServices?: {
|
||||
sig: Sig;
|
||||
srv: Srv;
|
||||
};
|
||||
|
||||
private _containsService(serviceId: string): boolean {
|
||||
return this._marineServices.has(serviceId) || this._commonHandlers.has(serviceId);
|
||||
}
|
||||
|
||||
// Internal peer state
|
||||
|
||||
private connection: FluenceConnection | null = null;
|
||||
private _printParticleId = false;
|
||||
private _defaultTTL: number = DEFAULT_TTL;
|
||||
private _keyPair: KeyPair | undefined;
|
||||
private _timeouts: Array<NodeJS.Timeout> = [];
|
||||
private _particleQueues = new Map<string, Subject<ParticleQueueItem>>();
|
||||
|
||||
private _onIncomingParticle(p: string) {
|
||||
const particle = Particle.fromString(p);
|
||||
this._incomingParticles.next({ particle, onStageChange: () => {} });
|
||||
}
|
||||
|
||||
private _startParticleProcessing() {
|
||||
this._incomingParticles
|
||||
.pipe(
|
||||
tap((x) => {
|
||||
x.particle.logTo('debug', 'particle received:');
|
||||
}),
|
||||
filterExpiredParticles(this._expireParticle.bind(this)),
|
||||
)
|
||||
.subscribe((item) => {
|
||||
const p = item.particle;
|
||||
let particlesQueue = this._particleQueues.get(p.id);
|
||||
|
||||
if (!particlesQueue) {
|
||||
particlesQueue = this._createParticlesProcessingQueue();
|
||||
this._particleQueues.set(p.id, particlesQueue);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
this._expireParticle(item);
|
||||
}, p.actualTtl());
|
||||
|
||||
this._timeouts.push(timeout);
|
||||
}
|
||||
|
||||
particlesQueue.next(item);
|
||||
});
|
||||
|
||||
this._outgoingParticles.subscribe((item) => {
|
||||
// Do not send particle after the peer has been stopped
|
||||
if (!this.getStatus().isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.connection) {
|
||||
item.particle.logTo('error', 'cannot send particle, peer is not connected');
|
||||
item.onStageChange({ stage: 'sendingError' });
|
||||
return;
|
||||
}
|
||||
item.particle.logTo('debug', 'sending particle:');
|
||||
this.connection?.sendParticle(item.nextPeerIds, item.particle.toString()).then(
|
||||
() => {
|
||||
item.onStageChange({ stage: 'sent' });
|
||||
},
|
||||
(e: any) => {
|
||||
log.error(e);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private _expireParticle(item: ParticleQueueItem) {
|
||||
const particleId = item.particle.id;
|
||||
log.debug(
|
||||
`particle ${particleId} has expired after ${item.particle.ttl}. Deleting particle-related queues and handlers`,
|
||||
);
|
||||
|
||||
this._particleQueues.delete(particleId);
|
||||
this._particleSpecificHandlers.delete(particleId);
|
||||
|
||||
item.onStageChange({ stage: 'expired' });
|
||||
}
|
||||
|
||||
private _createParticlesProcessingQueue() {
|
||||
const particlesQueue = new Subject<ParticleQueueItem>();
|
||||
let prevData: Uint8Array = Buffer.from([]);
|
||||
|
||||
particlesQueue
|
||||
.pipe(
|
||||
filterExpiredParticles(this._expireParticle.bind(this)),
|
||||
|
||||
concatMap(async (item) => {
|
||||
const status = this.getStatus();
|
||||
if (!status.isInitialized || this.marine === undefined) {
|
||||
// If `.stop()` was called return null to stop particle processing immediately
|
||||
return null;
|
||||
}
|
||||
|
||||
// IMPORTANT!
|
||||
// AVM runner execution and prevData <-> newData swapping
|
||||
// MUST happen sequentially (in a critical section).
|
||||
// Otherwise the race between runner might occur corrupting the prevData
|
||||
|
||||
item.particle.logTo('debug', 'Sending particle to interpreter');
|
||||
log.debug('prevData: ', dataToString(prevData));
|
||||
|
||||
const avmCallResult = await this.avmRunner.run(
|
||||
{
|
||||
initPeerId: item.particle.initPeerId,
|
||||
currentPeerId: status.peerId,
|
||||
timestamp: item.particle.timestamp,
|
||||
ttl: item.particle.ttl,
|
||||
},
|
||||
item.particle.script,
|
||||
prevData,
|
||||
item.particle.data,
|
||||
item.particle.callResults,
|
||||
);
|
||||
|
||||
if (!(avmCallResult instanceof Error) && avmCallResult.retCode === 0) {
|
||||
const newData = Buffer.from(avmCallResult.data);
|
||||
prevData = newData;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
result: avmCallResult,
|
||||
};
|
||||
}),
|
||||
)
|
||||
.subscribe((item) => {
|
||||
// If `.stop()` was called then item will be null and we need to stop particle processing immediately
|
||||
if (item === null || !this.getStatus().isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not proceed further if the particle is expired
|
||||
if (item.particle.hasExpired()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not continue if there was an error in particle interpretation
|
||||
if (item.result instanceof Error) {
|
||||
log.error('Interpreter failed: ', jsonify(item.result.message));
|
||||
item.onStageChange({ stage: 'interpreterError', errorMessage: item.result.message });
|
||||
return;
|
||||
}
|
||||
|
||||
const toLog = { ...item.result, data: dataToString(item.result.data) };
|
||||
if (item.result.retCode !== 0) {
|
||||
log.error('Interpreter failed: ', jsonify(toLog));
|
||||
item.onStageChange({ stage: 'interpreterError', errorMessage: item.result.errorMessage });
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug('Interpreter result: ', jsonify(toLog));
|
||||
|
||||
setTimeout(() => {
|
||||
item.onStageChange({ stage: 'interpreted' });
|
||||
}, 0);
|
||||
|
||||
// send particle further if requested
|
||||
if (item.result.nextPeerPks.length > 0) {
|
||||
const newParticle = item.particle.clone();
|
||||
const newData = Buffer.from(item.result.data);
|
||||
newParticle.data = newData;
|
||||
this._outgoingParticles.next({
|
||||
...item,
|
||||
particle: newParticle,
|
||||
nextPeerIds: item.result.nextPeerPks,
|
||||
});
|
||||
}
|
||||
|
||||
// execute call requests if needed
|
||||
// and put particle with the results back to queue
|
||||
if (item.result.callRequests.length > 0) {
|
||||
for (const [key, cr] of item.result.callRequests) {
|
||||
const req = {
|
||||
fnName: cr.functionName,
|
||||
args: cr.arguments,
|
||||
serviceId: cr.serviceId,
|
||||
tetraplets: cr.tetraplets,
|
||||
particleContext: item.particle.getParticleContext(),
|
||||
};
|
||||
|
||||
if (item.particle.hasExpired()) {
|
||||
// just in case do not call any services if the particle is already expired
|
||||
return;
|
||||
}
|
||||
this._execSingleCallRequest(req)
|
||||
.catch((err): CallServiceResult => {
|
||||
if (err instanceof ServiceError) {
|
||||
return {
|
||||
retCode: ResultCodes.error,
|
||||
result: err.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
retCode: ResultCodes.error,
|
||||
result: `Handler failed. fnName="${req.fnName}" serviceId="${
|
||||
req.serviceId
|
||||
}" error: ${err.toString()}`,
|
||||
};
|
||||
})
|
||||
.then((res) => {
|
||||
const serviceResult = {
|
||||
result: jsonify(res.result),
|
||||
retCode: res.retCode,
|
||||
};
|
||||
|
||||
const newParticle = item.particle.clone();
|
||||
newParticle.callResults = [[key, serviceResult]];
|
||||
newParticle.data = Buffer.from([]);
|
||||
|
||||
particlesQueue.next({ ...item, particle: newParticle });
|
||||
});
|
||||
}
|
||||
} else {
|
||||
item.onStageChange({ stage: 'localWorkDone' });
|
||||
}
|
||||
});
|
||||
|
||||
return particlesQueue;
|
||||
}
|
||||
|
||||
private async _execSingleCallRequest(req: CallServiceData): Promise<CallServiceResult> {
|
||||
log.debug('executing call service handler', jsonify(req));
|
||||
const particleId = req.particleContext.particleId;
|
||||
|
||||
if (this.marine && this._marineServices.has(req.serviceId)) {
|
||||
const result = await this.marine.callService(req.serviceId, req.fnName, req.args, undefined);
|
||||
|
||||
return {
|
||||
retCode: ResultCodes.success,
|
||||
result: result as JSONValue,
|
||||
};
|
||||
}
|
||||
|
||||
const key = serviceFnKey(req.serviceId, req.fnName);
|
||||
const psh = this._particleSpecificHandlers.get(particleId);
|
||||
let handler: GenericCallServiceHandler | undefined;
|
||||
|
||||
// we should prioritize handler for this particle if there is one
|
||||
// if particle-specific handlers exist for this particle try getting handler there
|
||||
if (psh !== undefined) {
|
||||
handler = psh.get(key);
|
||||
}
|
||||
|
||||
// then try to find a common handler for all particles with this service-fn key
|
||||
// if there is no particle-specific handler, get one from common map
|
||||
if (handler === undefined) {
|
||||
handler = this._commonHandlers.get(key);
|
||||
}
|
||||
|
||||
// if no handler is found return useful error message to AVM
|
||||
if (handler === undefined) {
|
||||
return {
|
||||
retCode: ResultCodes.error,
|
||||
result: `No handler has been registered for serviceId='${req.serviceId}' fnName='${
|
||||
req.fnName
|
||||
}' args='${jsonify(req.args)}'`,
|
||||
};
|
||||
}
|
||||
|
||||
// if we found a handler, execute it
|
||||
const res = await handler(req);
|
||||
|
||||
if (res.result === undefined) {
|
||||
res.result = null;
|
||||
}
|
||||
|
||||
log.debug('executed call service handler, req and res are: ', jsonify(req), jsonify(res));
|
||||
return res;
|
||||
}
|
||||
|
||||
private _stopParticleProcessing() {
|
||||
// do not hang if the peer has been stopped while some of the timeouts are still being executed
|
||||
this._timeouts.forEach((timeout) => {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
this._particleQueues.clear();
|
||||
}
|
||||
}
|
||||
|
||||
async function configToConnection(
|
||||
keyPair: KeyPair,
|
||||
connection?: RelayOptions,
|
||||
dialTimeoutMs?: number,
|
||||
): Promise<FluenceConnection | null> {
|
||||
if (!connection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (connection instanceof FluenceConnection) {
|
||||
return connection;
|
||||
}
|
||||
|
||||
let connectToMultiAddr: MultiaddrInput;
|
||||
// figuring out what was specified as input
|
||||
const tmp = connection as any;
|
||||
if (tmp.multiaddr !== undefined) {
|
||||
// specified as FluenceNode (object with multiaddr and peerId props)
|
||||
connectToMultiAddr = tmp.multiaddr;
|
||||
} else {
|
||||
// specified as MultiaddrInput
|
||||
connectToMultiAddr = tmp;
|
||||
}
|
||||
|
||||
const res = await RelayConnection.createConnection({
|
||||
peerId: keyPair.getLibp2pPeerId(),
|
||||
relayAddress: connectToMultiAddr,
|
||||
dialTimeoutMs: dialTimeoutMs,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
function serviceFnKey(serviceId: string, fnName: string) {
|
||||
return `${serviceId}/${fnName}`;
|
||||
}
|
||||
|
||||
function registerDefaultServices(peer: FluencePeer) {
|
||||
Object.entries(builtInServices).forEach(([serviceId, service]) => {
|
||||
Object.entries(service).forEach(([fnName, fn]) => {
|
||||
peer.internals.regHandler.common(serviceId, fnName, fn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function filterExpiredParticles(onParticleExpiration: (item: ParticleQueueItem) => void) {
|
||||
return pipe(
|
||||
tap((item: ParticleQueueItem) => {
|
||||
if (item.particle.hasExpired()) {
|
||||
onParticleExpiration(item);
|
||||
}
|
||||
}),
|
||||
filter((x: ParticleQueueItem) => !x.particle.hasExpired()),
|
||||
);
|
||||
}
|
||||
|
||||
async function makeKeyPair(opts?: KeyPairOptions) {
|
||||
opts = opts || { type: 'Ed25519', source: 'random' };
|
||||
return fromOpts(opts);
|
||||
}
|
153
packages/core/js-peer/src/js-peer/Particle.ts
Normal file
153
packages/core/js-peer/src/js-peer/Particle.ts
Normal file
@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2020 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { fromUint8Array, toUint8Array } from 'js-base64';
|
||||
import { CallResultsArray, LogLevel } from '@fluencelabs/avm';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import log from 'loglevel';
|
||||
import { ParticleContext } from '../interfaces/commonTypes.js';
|
||||
import { dataToString, jsonify } from './utils.js';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
export class Particle {
|
||||
// TODO: make it not optional (should be added to the constructor)
|
||||
signature?: string;
|
||||
callResults: CallResultsArray = [];
|
||||
|
||||
constructor(
|
||||
public id: string,
|
||||
public timestamp: number,
|
||||
public script: string,
|
||||
public data: Uint8Array,
|
||||
public ttl: number,
|
||||
public initPeerId: string,
|
||||
) {}
|
||||
|
||||
static createNew(script: string, ttl: number, initPeerId: string): Particle {
|
||||
return new Particle(genUUID(), Date.now(), script, Buffer.from([]), ttl, initPeerId);
|
||||
}
|
||||
|
||||
static fromString(str: string): Particle {
|
||||
const json = JSON.parse(str);
|
||||
const res = new Particle(
|
||||
json.id,
|
||||
json.timestamp,
|
||||
json.script,
|
||||
toUint8Array(json.data),
|
||||
json.ttl,
|
||||
json.init_peer_id,
|
||||
);
|
||||
|
||||
res.signature = json.signature;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
getParticleContext(): ParticleContext {
|
||||
return {
|
||||
particleId: this.id,
|
||||
initPeerId: this.initPeerId,
|
||||
timestamp: this.timestamp,
|
||||
ttl: this.ttl,
|
||||
signature: this.signature,
|
||||
};
|
||||
}
|
||||
|
||||
actualTtl(): number {
|
||||
return this.timestamp + this.ttl - Date.now();
|
||||
}
|
||||
|
||||
hasExpired(): boolean {
|
||||
return this.actualTtl() <= 0;
|
||||
}
|
||||
|
||||
clone(): Particle {
|
||||
const res = new Particle(this.id, this.timestamp, this.script, this.data, this.ttl, this.initPeerId);
|
||||
|
||||
res.signature = this.signature;
|
||||
res.callResults = this.callResults;
|
||||
return res;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify({
|
||||
action: 'Particle',
|
||||
id: this.id,
|
||||
init_peer_id: this.initPeerId,
|
||||
timestamp: this.timestamp,
|
||||
ttl: this.ttl,
|
||||
script: this.script,
|
||||
// TODO: copy signature from a particle after signatures will be implemented on nodes
|
||||
signature: [],
|
||||
data: this.data && fromUint8Array(this.data),
|
||||
});
|
||||
}
|
||||
|
||||
logTo(level: LogLevel, message: string) {
|
||||
let fn;
|
||||
let data: string | undefined;
|
||||
switch (level) {
|
||||
case 'debug':
|
||||
fn = log.debug;
|
||||
data = dataToString(this.data);
|
||||
break;
|
||||
case 'error':
|
||||
fn = log.error;
|
||||
break;
|
||||
case 'info':
|
||||
case 'trace':
|
||||
fn = log.info;
|
||||
break;
|
||||
case 'warn':
|
||||
fn = log.warn;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
fn(
|
||||
message,
|
||||
jsonify({
|
||||
id: this.id,
|
||||
init_peer_id: this.initPeerId,
|
||||
timestamp: this.timestamp,
|
||||
ttl: this.ttl,
|
||||
script: this.script,
|
||||
signature: this.signature,
|
||||
callResults: this.callResults,
|
||||
data,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type ParticleExecutionStage =
|
||||
| { stage: 'received' }
|
||||
| { stage: 'interpreted' }
|
||||
| { stage: 'interpreterError'; errorMessage: string }
|
||||
| { stage: 'localWorkDone' }
|
||||
| { stage: 'sent' }
|
||||
| { stage: 'sendingError' }
|
||||
| { stage: 'expired' };
|
||||
|
||||
export interface ParticleQueueItem {
|
||||
particle: Particle;
|
||||
onStageChange: (state: ParticleExecutionStage) => void;
|
||||
}
|
||||
|
||||
function genUUID() {
|
||||
return uuidv4();
|
||||
}
|
17
packages/core/js-peer/src/js-peer/__test__/connection.ts
Normal file
17
packages/core/js-peer/src/js-peer/__test__/connection.ts
Normal file
@ -0,0 +1,17 @@
|
||||
// Uncomment to test on dev nodes
|
||||
// import { krasnodar } from '@fluencelabs/fluence-network-environment';
|
||||
// export const nodes = krasnodar;
|
||||
|
||||
/*
|
||||
* start docker container to run integration tests locally:
|
||||
|
||||
docker run --rm -e RUST_LOG="info" -p 1210:1210 -p 4310:4310 fluencelabs/fluence -t 1210 -w 4310 -k gKdiCSUr1TFGFEgu2t8Ch1XEUsrN5A2UfBLjSZvfci9SPR3NvZpACfcpPGC3eY4zma1pk7UvYv5zb1VjvPHwCjj --local
|
||||
|
||||
*/
|
||||
|
||||
export const nodes = [
|
||||
{
|
||||
multiaddr: '/ip4/127.0.0.1/tcp/4310/ws/p2p/12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3',
|
||||
peerId: '12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3',
|
||||
},
|
||||
];
|
BIN
packages/core/js-peer/src/js-peer/__test__/data/greeting-record.wasm
Executable file
BIN
packages/core/js-peer/src/js-peer/__test__/data/greeting-record.wasm
Executable file
Binary file not shown.
BIN
packages/core/js-peer/src/js-peer/__test__/data/greeting.wasm
Executable file
BIN
packages/core/js-peer/src/js-peer/__test__/data/greeting.wasm
Executable file
Binary file not shown.
@ -0,0 +1,26 @@
|
||||
data GreetingRecord:
|
||||
str: string
|
||||
num: i32
|
||||
|
||||
service Greeting("greeting"):
|
||||
greeting(name: string) -> string
|
||||
greeting_record() -> GreetingRecord
|
||||
|
||||
func call(arg: string) -> string:
|
||||
res1 <- Greeting.greeting(arg)
|
||||
res2 <- Greeting.greeting(res1)
|
||||
res3 <- Greeting.greeting(res2)
|
||||
<- res3
|
||||
|
||||
service GreetingRecord:
|
||||
greeting_record() -> GreetingRecord
|
||||
log_debug()
|
||||
log_error()
|
||||
log_info()
|
||||
log_trace()
|
||||
log_warn()
|
||||
void_fn()
|
||||
|
||||
func call_info(srvId: string):
|
||||
GreetingRecord srvId
|
||||
GreetingRecord.log_info()
|
@ -0,0 +1,13 @@
|
||||
module Export
|
||||
|
||||
import SignResult, Sig from "../../../aqua/services.aqua"
|
||||
export Sig, DataProvider, callSig
|
||||
|
||||
service DataProvider("data"):
|
||||
provide_data() -> []u8
|
||||
|
||||
func callSig(sigId: string) -> SignResult:
|
||||
data <- DataProvider.provide_data()
|
||||
Sig sigId
|
||||
signature <- Sig.sign(data)
|
||||
<- signature
|
44
packages/core/js-peer/src/js-peer/__test__/data/srv.aqua
Normal file
44
packages/core/js-peer/src/js-peer/__test__/data/srv.aqua
Normal file
@ -0,0 +1,44 @@
|
||||
module Export
|
||||
|
||||
import Srv from "../../../aqua/single-module-srv.aqua"
|
||||
import NodeUtils from "../../../aqua/node-utils.aqua"
|
||||
export happy_path, list_services, file_not_found, service_removed, removing_non_exiting
|
||||
|
||||
service Greeting("greeting"):
|
||||
greeting(name: string) -> string
|
||||
|
||||
func happy_path(file_path: string) -> string:
|
||||
file <- NodeUtils.read_file(file_path)
|
||||
created_service <- Srv.create(file.content!)
|
||||
Greeting created_service.service_id!
|
||||
<- Greeting.greeting("test")
|
||||
|
||||
func list_services(file_path: string) -> []string:
|
||||
file <- NodeUtils.read_file(file_path)
|
||||
Srv.create(file.content!)
|
||||
Srv.create(file.content!)
|
||||
Srv.create(file.content!)
|
||||
<- Srv.list()
|
||||
|
||||
func file_not_found() -> string:
|
||||
e <- NodeUtils.read_file("/random/incorrect/file")
|
||||
<- e.error!
|
||||
|
||||
func service_removed(file_path: string) -> string:
|
||||
result: *string
|
||||
|
||||
file <- NodeUtils.read_file(file_path)
|
||||
created_service <- Srv.create(file.content!)
|
||||
Greeting created_service.service_id!
|
||||
Srv.remove(created_service.service_id!)
|
||||
try:
|
||||
dontcare <- Greeting.greeting("test")
|
||||
result <<- "ok"
|
||||
catch e:
|
||||
result <<- e.message
|
||||
<- result!
|
||||
|
||||
func removing_non_exiting() -> string:
|
||||
e <- Srv.remove("random_id")
|
||||
<- e.error!
|
||||
|
@ -0,0 +1,158 @@
|
||||
import { handleTimeout } from '../../utils.js';
|
||||
import { registerHandlersHelper, withPeer } from '../util.js';
|
||||
|
||||
describe('Avm spec', () => {
|
||||
it('Simple call', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
const res = await new Promise<string[]>((resolve, reject) => {
|
||||
const script = `
|
||||
(call %init_peer_id% ("print" "print") ["1"])
|
||||
`;
|
||||
const particle = peer.internals.createNewParticle(script);
|
||||
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
||||
registerHandlersHelper(peer, particle, {
|
||||
print: {
|
||||
print: (args: Array<Array<string>>) => {
|
||||
const [res] = args;
|
||||
resolve(res);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
peer.internals.initiateParticle(particle, handleTimeout(reject));
|
||||
});
|
||||
|
||||
expect(res).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
it('Par call', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
const res = await new Promise<string[]>((resolve, reject) => {
|
||||
const res: any[] = [];
|
||||
const script = `
|
||||
(seq
|
||||
(par
|
||||
(call %init_peer_id% ("print" "print") ["1"])
|
||||
(null)
|
||||
)
|
||||
(call %init_peer_id% ("print" "print") ["2"])
|
||||
)
|
||||
`;
|
||||
const particle = peer.internals.createNewParticle(script);
|
||||
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
||||
registerHandlersHelper(peer, particle, {
|
||||
print: {
|
||||
print: (args: any) => {
|
||||
res.push(args[0]);
|
||||
if (res.length == 2) {
|
||||
resolve(res);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
peer.internals.initiateParticle(particle, handleTimeout(reject));
|
||||
});
|
||||
|
||||
expect(res).toStrictEqual(['1', '2']);
|
||||
});
|
||||
});
|
||||
|
||||
it('Timeout in par call: race', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
const res = await new Promise((resolve, reject) => {
|
||||
const script = `
|
||||
(seq
|
||||
(call %init_peer_id% ("op" "identity") ["slow_result"] arg)
|
||||
(seq
|
||||
(par
|
||||
(call %init_peer_id% ("peer" "timeout") [1000 arg] $result)
|
||||
(call %init_peer_id% ("op" "identity") ["fast_result"] $result)
|
||||
)
|
||||
(seq
|
||||
(canon %init_peer_id% $result #result)
|
||||
(call %init_peer_id% ("return" "return") [#result.$[0]])
|
||||
)
|
||||
)
|
||||
)
|
||||
`;
|
||||
const particle = peer.internals.createNewParticle(script);
|
||||
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
||||
registerHandlersHelper(peer, particle, {
|
||||
return: {
|
||||
return: (args: any) => {
|
||||
resolve(args[0]);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
peer.internals.initiateParticle(particle, handleTimeout(reject));
|
||||
});
|
||||
|
||||
expect(res).toBe('fast_result');
|
||||
});
|
||||
});
|
||||
|
||||
it('Timeout in par call: wait', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
const res = await new Promise((resolve, reject) => {
|
||||
const script = `
|
||||
(seq
|
||||
(call %init_peer_id% ("op" "identity") ["timeout_msg"] arg)
|
||||
(seq
|
||||
(seq
|
||||
(par
|
||||
(call %init_peer_id% ("peer" "timeout") [1000 arg] $ok_or_err)
|
||||
(call "invalid_peer" ("op" "identity") ["never"] $ok_or_err)
|
||||
)
|
||||
(xor
|
||||
(seq
|
||||
(canon %init_peer_id% $ok_or_err #ok_or_err)
|
||||
(match #ok_or_err.$[0] "timeout_msg"
|
||||
(ap "failed_with_timeout" $result)
|
||||
)
|
||||
)
|
||||
(ap "impossible happened" $result)
|
||||
)
|
||||
)
|
||||
(seq
|
||||
(canon %init_peer_id% $result #result)
|
||||
(call %init_peer_id% ("return" "return") [#result.$[0]])
|
||||
)
|
||||
)
|
||||
)
|
||||
`;
|
||||
const particle = peer.internals.createNewParticle(script);
|
||||
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
||||
registerHandlersHelper(peer, particle, {
|
||||
return: {
|
||||
return: (args: any) => {
|
||||
resolve(args[0]);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
peer.internals.initiateParticle(particle, handleTimeout(reject));
|
||||
});
|
||||
|
||||
expect(res).toBe('failed_with_timeout');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,76 @@
|
||||
import { Particle } from '../../Particle.js';
|
||||
import { doNothing } from '../../utils.js';
|
||||
import { FluencePeer } from '../../FluencePeer.js';
|
||||
import { mkTestPeer } from '../util.js';
|
||||
|
||||
let peer: FluencePeer;
|
||||
|
||||
describe('Sig service test suite', () => {
|
||||
afterEach(async () => {
|
||||
if (peer) {
|
||||
await peer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
peer = mkTestPeer();
|
||||
await peer.start();
|
||||
});
|
||||
|
||||
it('JSON builtin spec', async () => {
|
||||
const script = `
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
;; create
|
||||
(seq
|
||||
(call %init_peer_id% ("json" "obj") ["name" "nested_first" "num" 1] nested_first)
|
||||
(call %init_peer_id% ("json" "obj") ["name" "nested_second" "num" 2] nested_second)
|
||||
)
|
||||
(call %init_peer_id% ("json" "obj") ["name" "outer_first" "num" 0 "nested" nested_first] outer_first)
|
||||
)
|
||||
(seq
|
||||
;; modify
|
||||
(seq
|
||||
(call %init_peer_id% ("json" "put") [outer_first "nested" nested_second] outer_tmp_second)
|
||||
(call %init_peer_id% ("json" "puts") [outer_tmp_second "name" "outer_second" "num" 3] outer_second)
|
||||
)
|
||||
;; stringify and parse
|
||||
(seq
|
||||
(call %init_peer_id% ("json" "stringify") [outer_first] outer_first_string)
|
||||
(call %init_peer_id% ("json" "parse") [outer_first_string] outer_first_parsed)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call %init_peer_id% ("res" "res") [nested_first nested_second outer_first outer_second outer_first_string outer_first_parsed])
|
||||
)
|
||||
`;
|
||||
const promise = new Promise<any>((resolve) => {
|
||||
peer.internals.regHandler.common('res', 'res', (req) => {
|
||||
resolve(req.args);
|
||||
return {
|
||||
result: {},
|
||||
retCode: 0,
|
||||
};
|
||||
});
|
||||
});
|
||||
const p = peer.internals.createNewParticle(script) as Particle;
|
||||
await peer.internals.initiateParticle(p, doNothing);
|
||||
|
||||
const [nestedFirst, nestedSecond, outerFirst, outerSecond, outerFirstString, outerFirstParsed] = await promise;
|
||||
|
||||
const nfExpected = { name: 'nested_first', num: 1 };
|
||||
const nsExpected = { name: 'nested_second', num: 2 };
|
||||
|
||||
const ofExpected = { name: 'outer_first', nested: nfExpected, num: 0 };
|
||||
const ofString = JSON.stringify(ofExpected);
|
||||
const osExpected = { name: 'outer_second', num: 3, nested: nsExpected };
|
||||
|
||||
expect(nestedFirst).toMatchObject(nfExpected);
|
||||
expect(nestedSecond).toMatchObject(nsExpected);
|
||||
expect(outerFirst).toMatchObject(ofExpected);
|
||||
expect(outerSecond).toMatchObject(osExpected);
|
||||
expect(outerFirstParsed).toMatchObject(ofExpected);
|
||||
expect(outerFirstString).toBe(ofString);
|
||||
});
|
||||
});
|
@ -0,0 +1,53 @@
|
||||
import * as fs from 'fs';
|
||||
import * as url from 'url';
|
||||
import * as path from 'path';
|
||||
import { compileAqua, withPeer } from '../util.js';
|
||||
|
||||
let aqua: any;
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
describe('Marine js tests', () => {
|
||||
beforeAll(async () => {
|
||||
const { services, functions } = await compileAqua(path.join(__dirname, '../data/marine-js.aqua'));
|
||||
aqua = functions;
|
||||
});
|
||||
|
||||
it('should call marine service correctly', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
// arrange
|
||||
const wasm = await fs.promises.readFile(path.join(__dirname, '../data/greeting.wasm'));
|
||||
await peer.registerMarineService(wasm, 'greeting');
|
||||
|
||||
// act
|
||||
const res = await aqua.call(peer, { arg: 'test' });
|
||||
|
||||
// assert
|
||||
expect(res).toBe('Hi, Hi, Hi, test');
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: console printouts are happening inside web-worker\worker threads.
|
||||
// Find a way to mock functions in background thread
|
||||
it.skip('logging should work', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
// arrange
|
||||
|
||||
jest.spyOn(global.console, 'info').mockImplementation(() => {});
|
||||
|
||||
await peer.start({
|
||||
debug: {
|
||||
marineLogLevel: 'debug',
|
||||
},
|
||||
});
|
||||
const wasm = await fs.promises.readFile(path.join(__dirname, '../data/greeting-record.wasm'));
|
||||
await peer.registerMarineService(wasm, 'greeting');
|
||||
|
||||
// act
|
||||
await aqua.call_info(peer, { arg: 'greeting' });
|
||||
|
||||
// assert
|
||||
expect(console.info).toBeCalledTimes(1);
|
||||
expect(console.info).toHaveBeenNthCalledWith(1, '[marine service "greeting"]: info');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,425 @@
|
||||
import { nodes } from '../connection.js';
|
||||
import { checkConnection, doNothing, handleTimeout } from '../../utils.js';
|
||||
import { registerHandlersHelper, mkTestPeer, withPeer, withConnectedPeer } from '../util.js';
|
||||
import { FluencePeer } from '../../FluencePeer.js';
|
||||
|
||||
describe('Typescript usage suite', () => {
|
||||
it('should perform test for FluencePeer class correctly', () => {
|
||||
// arrange
|
||||
const peer = mkTestPeer();
|
||||
const number = 1;
|
||||
const object = { str: 'Hello!' };
|
||||
const undefinedVal = undefined;
|
||||
|
||||
// act
|
||||
const isPeerPeer = FluencePeer.isInstance(peer);
|
||||
const isNumberPeer = FluencePeer.isInstance(number);
|
||||
const isObjectPeer = FluencePeer.isInstance(object);
|
||||
const isUndefinedPeer = FluencePeer.isInstance(undefinedVal);
|
||||
|
||||
// act
|
||||
expect(isPeerPeer).toBe(true);
|
||||
expect(isNumberPeer).toBe(false);
|
||||
expect(isObjectPeer).toBe(false);
|
||||
expect(isUndefinedPeer).toBe(false);
|
||||
});
|
||||
|
||||
describe('Should expose correct peer status', () => {
|
||||
it('Should expose correct status for uninitialized peer', () => {
|
||||
const peer = mkTestPeer();
|
||||
const status = peer.getStatus();
|
||||
|
||||
expect(status.isConnected).toBe(false);
|
||||
expect(status.isInitialized).toBe(false);
|
||||
expect(status.peerId).toBe(null);
|
||||
expect(status.relayPeerId).toBe(null);
|
||||
});
|
||||
|
||||
it('Should expose correct status for initialized but not connected peer', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
// arrange
|
||||
|
||||
// act
|
||||
const status = peer.getStatus();
|
||||
|
||||
// assert
|
||||
expect(status.isConnected).toBe(false);
|
||||
expect(status.isInitialized).toBe(true);
|
||||
expect(status.peerId).not.toBe(null);
|
||||
expect(status.relayPeerId).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should expose correct status for connected peer', async () => {
|
||||
await withConnectedPeer(async (peer) => {
|
||||
// arrange
|
||||
|
||||
// act
|
||||
const status = peer.getStatus();
|
||||
|
||||
// assert
|
||||
expect(status.isConnected).toBe(true);
|
||||
expect(status.isInitialized).toBe(true);
|
||||
expect(status.peerId).not.toBe(null);
|
||||
expect(status.relayPeerId).not.toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should make a call through network', async () => {
|
||||
await withConnectedPeer(async (peer) => {
|
||||
// arrange
|
||||
|
||||
const result = await new Promise<string[]>((resolve, reject) => {
|
||||
const script = `
|
||||
(xor
|
||||
(seq
|
||||
(call %init_peer_id% ("load" "relay") [] init_relay)
|
||||
(seq
|
||||
(call init_relay ("op" "identity") ["hello world!"] result)
|
||||
(call %init_peer_id% ("callback" "callback") [result])
|
||||
)
|
||||
)
|
||||
(seq
|
||||
(call init_relay ("op" "identity") [])
|
||||
(call %init_peer_id% ("callback" "error") [%last_error%])
|
||||
)
|
||||
)`;
|
||||
const particle = peer.internals.createNewParticle(script);
|
||||
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
||||
registerHandlersHelper(peer, particle, {
|
||||
load: {
|
||||
relay: () => {
|
||||
return peer.getStatus().relayPeerId;
|
||||
},
|
||||
},
|
||||
callback: {
|
||||
callback: (args: any) => {
|
||||
const [val] = args;
|
||||
resolve(val);
|
||||
},
|
||||
error: (args: any) => {
|
||||
const [error] = args;
|
||||
reject(error);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
peer.internals.initiateParticle(particle, handleTimeout(reject));
|
||||
});
|
||||
|
||||
expect(result).toBe('hello world!');
|
||||
});
|
||||
});
|
||||
|
||||
it('check connection should work', async function () {
|
||||
await withConnectedPeer(async (peer) => {
|
||||
const isConnected = await checkConnection(peer);
|
||||
|
||||
expect(isConnected).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('check connection should work with ttl', async function () {
|
||||
await withConnectedPeer(async (peer) => {
|
||||
const isConnected = await checkConnection(peer, 10000);
|
||||
|
||||
expect(isConnected).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('two clients should work inside the same time browser', async () => {
|
||||
await withConnectedPeer(async (peer1) => {
|
||||
await withConnectedPeer(async (peer2) => {
|
||||
const res = new Promise((resolve) => {
|
||||
peer2.internals.regHandler.common('test', 'test', (req) => {
|
||||
resolve(req.args[0]);
|
||||
return {
|
||||
result: {},
|
||||
retCode: 0,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const script = `
|
||||
(seq
|
||||
(call "${peer1.getStatus().relayPeerId}" ("op" "identity") [])
|
||||
(call "${peer2.getStatus().peerId}" ("test" "test") ["test"])
|
||||
)
|
||||
`;
|
||||
const particle = peer1.internals.createNewParticle(script);
|
||||
|
||||
if (particle instanceof Error) {
|
||||
throw particle;
|
||||
}
|
||||
|
||||
peer1.internals.initiateParticle(particle, doNothing);
|
||||
|
||||
expect(await res).toEqual('test');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should make connection to network', () => {
|
||||
it('address as string', async () => {
|
||||
await withConnectedPeer(async (peer) => {
|
||||
const isConnected = await checkConnection(peer);
|
||||
|
||||
expect(isConnected).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('address as multiaddr', async () => {
|
||||
await withConnectedPeer(async (peer) => {
|
||||
const isConnected = await checkConnection(peer);
|
||||
|
||||
expect(isConnected).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('address as node', async () => {
|
||||
await withConnectedPeer(async (peer) => {
|
||||
const isConnected = await checkConnection(peer);
|
||||
|
||||
expect(isConnected).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('With connection options: dialTimeout', async () => {
|
||||
await withPeer(
|
||||
async (peer) => {
|
||||
const isConnected = await checkConnection(peer);
|
||||
|
||||
expect(isConnected).toBeTruthy();
|
||||
},
|
||||
{ connectTo: nodes[0], dialTimeoutMs: 100000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('With connection options: skipCheckConnection', async () => {
|
||||
await withPeer(
|
||||
async (peer) => {
|
||||
const isConnected = await checkConnection(peer);
|
||||
|
||||
expect(isConnected).toBeTruthy();
|
||||
},
|
||||
{ connectTo: nodes[0], skipCheckConnection: true },
|
||||
);
|
||||
});
|
||||
|
||||
it('With connection options: checkConnectionTTL', async () => {
|
||||
await withPeer(
|
||||
async (peer) => {
|
||||
const isConnected = await checkConnection(peer);
|
||||
|
||||
expect(isConnected).toBeTruthy();
|
||||
},
|
||||
{ connectTo: nodes[0], checkConnectionTimeoutMs: 1000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('With connection options: defaultTTL', async () => {
|
||||
await withPeer(
|
||||
async (peer) => {
|
||||
const isConnected = await checkConnection(peer);
|
||||
|
||||
expect(isConnected).toBeFalsy();
|
||||
},
|
||||
{ connectTo: nodes[0], defaultTtlMs: 1 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should successfully call identity on local peer', async function () {
|
||||
await withPeer(async (peer) => {
|
||||
const res = await new Promise<string>((resolve, reject) => {
|
||||
const script = `
|
||||
(seq
|
||||
(call %init_peer_id% ("op" "identity") ["test"] res)
|
||||
(call %init_peer_id% ("callback" "callback") [res])
|
||||
)
|
||||
`;
|
||||
const particle = peer.internals.createNewParticle(script);
|
||||
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
||||
registerHandlersHelper(peer, particle, {
|
||||
callback: {
|
||||
callback: async (args: any) => {
|
||||
const [res] = args;
|
||||
resolve(res);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
peer.internals.initiateParticle(particle, handleTimeout(reject));
|
||||
});
|
||||
|
||||
expect(res).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should throw correct message when calling non existing local service', async function () {
|
||||
await withConnectedPeer(async (peer) => {
|
||||
const res = callIncorrectService(peer);
|
||||
|
||||
await expect(res).rejects.toMatchObject({
|
||||
message: expect.stringContaining(
|
||||
`No handler has been registered for serviceId='incorrect' fnName='incorrect' args='[]'\"'`,
|
||||
),
|
||||
// instruction: 'call %init_peer_id% ("incorrect" "incorrect") [] res',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Should not crash if undefined is passed as a variable', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
const res = await new Promise<any>((resolve, reject) => {
|
||||
const script = `
|
||||
(seq
|
||||
(call %init_peer_id% ("load" "arg") [] arg)
|
||||
(seq
|
||||
(call %init_peer_id% ("op" "identity") [arg] res)
|
||||
(call %init_peer_id% ("callback" "callback") [res])
|
||||
)
|
||||
)`;
|
||||
const particle = peer.internals.createNewParticle(script);
|
||||
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
||||
registerHandlersHelper(peer, particle, {
|
||||
load: {
|
||||
arg: () => undefined,
|
||||
},
|
||||
callback: {
|
||||
callback: (args: any) => {
|
||||
const [val] = args;
|
||||
resolve(val);
|
||||
},
|
||||
error: (args: any) => {
|
||||
const [error] = args;
|
||||
reject(error);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
peer.internals.initiateParticle(particle, handleTimeout(reject));
|
||||
});
|
||||
|
||||
expect(res).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should not crash if an error ocurred in user-defined handler', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
const promise = new Promise<any>((_resolve, reject) => {
|
||||
const script = `
|
||||
(xor
|
||||
(call %init_peer_id% ("load" "arg") [] arg)
|
||||
(call %init_peer_id% ("callback" "error") [%last_error%])
|
||||
)`;
|
||||
const particle = peer.internals.createNewParticle(script);
|
||||
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
||||
registerHandlersHelper(peer, particle, {
|
||||
load: {
|
||||
arg: () => {
|
||||
throw new Error('my super custom error message');
|
||||
},
|
||||
},
|
||||
callback: {
|
||||
error: (args: any) => {
|
||||
const [error] = args;
|
||||
reject(error);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
peer.internals.initiateParticle(particle, handleTimeout(reject));
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toMatchObject({
|
||||
message: expect.stringContaining('my super custom error message'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Should return error if particle is created on a stopped peer', async () => {
|
||||
const peer = mkTestPeer();
|
||||
const particle = peer.internals.createNewParticle(`(null)`);
|
||||
|
||||
expect(particle instanceof Error).toBe(true);
|
||||
});
|
||||
|
||||
it.skip('Should throw correct error when the client tries to send a particle not to the relay', async () => {
|
||||
await withConnectedPeer(async (peer) => {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const script = `
|
||||
(xor
|
||||
(call "incorrect_peer_id" ("any" "service") [])
|
||||
(call %init_peer_id% ("callback" "error") [%last_error%])
|
||||
)`;
|
||||
const particle = peer.internals.createNewParticle(script);
|
||||
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
||||
registerHandlersHelper(peer, particle, {
|
||||
callback: {
|
||||
error: (args: any) => {
|
||||
const [error] = args;
|
||||
reject(error);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
peer.internals.initiateParticle(particle, doNothing);
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toMatch(
|
||||
'Particle is expected to be sent to only the single peer (relay which client is connected to)',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function callIncorrectService(peer: FluencePeer): Promise<string[]> {
|
||||
return new Promise<any[]>((resolve, reject) => {
|
||||
const script = `
|
||||
(xor
|
||||
(call %init_peer_id% ("incorrect" "incorrect") [] res)
|
||||
(call %init_peer_id% ("callback" "error") [%last_error%])
|
||||
)`;
|
||||
const particle = peer.internals.createNewParticle(script);
|
||||
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
||||
registerHandlersHelper(peer, particle, {
|
||||
callback: {
|
||||
callback: (args: any) => {
|
||||
resolve(args);
|
||||
},
|
||||
error: (args: any) => {
|
||||
const [error] = args;
|
||||
reject(error);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
peer.internals.initiateParticle(particle, handleTimeout(reject));
|
||||
});
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
import { KeyPair } from '../../../keypair/index.js';
|
||||
import { allowServiceFn } from '../../builtins/securityGuard.js';
|
||||
import { Sig } from '../../builtins/Sig.js';
|
||||
import { compileAqua, withPeer } from '../util.js';
|
||||
import { registerServiceImpl } from '../../compilerSupport/registerService.js';
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
let aqua: any;
|
||||
let sigDef: any;
|
||||
let dataProviderDef: any;
|
||||
|
||||
describe('Sig service test suite', () => {
|
||||
beforeAll(async () => {
|
||||
const { services, functions } = await compileAqua(path.join(__dirname, '../data/sigService.aqua'));
|
||||
aqua = functions;
|
||||
sigDef = services.Sig;
|
||||
dataProviderDef = services.DataProvider;
|
||||
});
|
||||
|
||||
it('Use custom sig service, success path', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
const customKeyPair = await KeyPair.randomEd25519();
|
||||
const customSig = new Sig(customKeyPair);
|
||||
const data = [1, 2, 3, 4, 5];
|
||||
|
||||
registerServiceImpl(peer, sigDef, 'CustomSig', customSig);
|
||||
|
||||
registerServiceImpl(peer, dataProviderDef, 'data', {
|
||||
provide_data: () => {
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
customSig.securityGuard = allowServiceFn('data', 'provide_data');
|
||||
|
||||
const result = await aqua.callSig(peer, { sigId: 'CustomSig' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const isSigCorrect = await customSig.verify(result.signature as number[], data);
|
||||
expect(isSigCorrect).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('Use custom sig service, fail path', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
const customKeyPair = await KeyPair.randomEd25519();
|
||||
const customSig = new Sig(customKeyPair);
|
||||
const data = [1, 2, 3, 4, 5];
|
||||
|
||||
registerServiceImpl(peer, sigDef, 'CustomSig', customSig);
|
||||
|
||||
registerServiceImpl(peer, dataProviderDef, 'data', {
|
||||
provide_data: () => {
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
customSig.securityGuard = allowServiceFn('wrong', 'wrong');
|
||||
|
||||
const result = await aqua.callSig(peer, { sigId: 'CustomSig' });
|
||||
});
|
||||
});
|
||||
|
||||
it('Default sig service should be resolvable by peer id', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
const sig = peer.getServices().sig;
|
||||
|
||||
const data = [1, 2, 3, 4, 5];
|
||||
registerServiceImpl(peer, dataProviderDef, 'data', {
|
||||
provide_data: () => {
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
const callAsSigRes = await aqua.callSig(peer, { sigId: 'sig' });
|
||||
const callAsPeerIdRes = await aqua.callSig(peer, { sigId: peer.getStatus().peerId });
|
||||
|
||||
expect(callAsSigRes.success).toBe(false);
|
||||
expect(callAsPeerIdRes.success).toBe(false);
|
||||
|
||||
sig.securityGuard = () => true;
|
||||
|
||||
const callAsSigResAfterGuardChange = await aqua.callSig(peer, { sigId: 'sig' });
|
||||
const callAsPeerIdResAfterGuardChange = await aqua.callSig(peer, {
|
||||
sigId: peer.getStatus().peerId,
|
||||
});
|
||||
|
||||
expect(callAsSigResAfterGuardChange.success).toBe(true);
|
||||
expect(callAsPeerIdResAfterGuardChange.success).toBe(true);
|
||||
|
||||
const isValid = await sig.verify(callAsSigResAfterGuardChange.signature as number[], data);
|
||||
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,67 @@
|
||||
import { handleTimeout } from '../../utils.js';
|
||||
import { nodes } from '../connection.js';
|
||||
import { mkTestPeer, registerHandlersHelper } from '../util.js';
|
||||
|
||||
const smokeTest = async () => {
|
||||
// arrange
|
||||
const peer = mkTestPeer();
|
||||
await peer.start({
|
||||
relay: nodes[0],
|
||||
});
|
||||
|
||||
const result = await new Promise<string[]>((resolve, reject) => {
|
||||
const script = `
|
||||
(xor
|
||||
(seq
|
||||
(call %init_peer_id% ("load" "relay") [] init_relay)
|
||||
(seq
|
||||
(call init_relay ("op" "identity") ["hello world!"] result)
|
||||
(call %init_peer_id% ("callback" "callback") [result])
|
||||
)
|
||||
)
|
||||
(seq
|
||||
(call init_relay ("op" "identity") [])
|
||||
(call %init_peer_id% ("callback" "error") [%last_error%])
|
||||
)
|
||||
)`;
|
||||
const particle = peer.internals.createNewParticle(script);
|
||||
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
||||
registerHandlersHelper(peer, particle, {
|
||||
load: {
|
||||
relay: () => {
|
||||
return peer.getStatus().relayPeerId;
|
||||
},
|
||||
},
|
||||
callback: {
|
||||
callback: (args: any) => {
|
||||
const [val] = args;
|
||||
resolve(val);
|
||||
},
|
||||
error: (args: any) => {
|
||||
const [error] = args;
|
||||
reject(error);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
peer.internals.initiateParticle(particle, handleTimeout(reject));
|
||||
});
|
||||
|
||||
await peer.stop();
|
||||
|
||||
if (result[0] !== 'hello world!') {
|
||||
throw new Error('Expecting "hello wrold!" got ' + result[0]);
|
||||
}
|
||||
};
|
||||
|
||||
smokeTest()
|
||||
.then(() => {
|
||||
console.log('Test passed');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Test failed: ', err);
|
||||
});
|
@ -0,0 +1,76 @@
|
||||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
import { compileAqua, withPeer } from '../util.js';
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
||||
let aqua: any;
|
||||
|
||||
describe('Srv service test suite', () => {
|
||||
beforeAll(async () => {
|
||||
const { services, functions } = await compileAqua(path.join(__dirname, '../data/srv.aqua'));
|
||||
aqua = functions;
|
||||
});
|
||||
|
||||
it('Use custom srv service, success path', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
// arrange
|
||||
const wasm = path.join(__dirname, '../data/greeting.wasm');
|
||||
|
||||
// act
|
||||
const res = await aqua.happy_path(peer, { file_path: wasm });
|
||||
|
||||
// assert
|
||||
expect(res).toBe('Hi, test');
|
||||
});
|
||||
});
|
||||
|
||||
it('List deployed services', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
// arrange
|
||||
const wasm = path.join(__dirname, '../data/greeting.wasm');
|
||||
|
||||
// act
|
||||
const res = await aqua.list_services(peer, { file_path: wasm });
|
||||
|
||||
// assert
|
||||
expect(res).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('Correct error for removed services', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
// arrange
|
||||
const wasm = path.join(__dirname, '../data/greeting.wasm');
|
||||
|
||||
// act
|
||||
const res = await aqua.service_removed(peer, { file_path: wasm });
|
||||
|
||||
// assert
|
||||
expect(res).toMatch('No handler has been registered for serviceId');
|
||||
});
|
||||
});
|
||||
|
||||
it('Correct error for file not found', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
// arrange
|
||||
|
||||
// act
|
||||
const res = await aqua.file_not_found(peer, {});
|
||||
|
||||
// assert
|
||||
expect(res).toMatch("ENOENT: no such file or directory, open '/random/incorrect/file'");
|
||||
});
|
||||
});
|
||||
|
||||
it('Correct error for removing non existing service', async () => {
|
||||
await withPeer(async (peer) => {
|
||||
// arrange
|
||||
|
||||
// act
|
||||
const res = await aqua.removing_non_exiting(peer, {});
|
||||
|
||||
// assert
|
||||
expect(res).toMatch('Service with id random_id not found');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,33 @@
|
||||
import { mkTestPeer } from '../util.js';
|
||||
|
||||
const peer = mkTestPeer();
|
||||
|
||||
describe('Parse ast tests', () => {
|
||||
beforeAll(async () => {
|
||||
await peer.start();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await peer.stop();
|
||||
});
|
||||
|
||||
it('Correct ast should be parsed correctly', async function () {
|
||||
const air = `(null)`;
|
||||
const res = await peer.internals.parseAst(air);
|
||||
|
||||
expect(res).toStrictEqual({
|
||||
success: true,
|
||||
data: { Null: null },
|
||||
});
|
||||
});
|
||||
|
||||
it('Incorrect ast should result in corresponding error', async function () {
|
||||
const air = `(null`;
|
||||
const res = await peer.internals.parseAst(air);
|
||||
|
||||
expect(res).toStrictEqual({
|
||||
success: false,
|
||||
data: expect.stringContaining('error'),
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,305 @@
|
||||
import { CallParams } from '@fluencelabs/interfaces';
|
||||
import { toUint8Array } from 'js-base64';
|
||||
import { CallServiceData } from '../../../interfaces/commonTypes.js';
|
||||
import { builtInServices } from '../../builtins/common.js';
|
||||
import { KeyPair } from '../../../keypair/index.js';
|
||||
import { Sig, defaultSigGuard } from '../../builtins/Sig.js';
|
||||
import { allowServiceFn } from '../../builtins/securityGuard.js';
|
||||
|
||||
const a10b20 = `{
|
||||
"a": 10,
|
||||
"b": 20
|
||||
}`;
|
||||
|
||||
const oneTwoThreeFour = `[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]`;
|
||||
|
||||
describe('Tests for default handler', () => {
|
||||
test.each`
|
||||
serviceId | fnName | args | retCode | result
|
||||
${'op'} | ${'identity'} | ${[]} | ${0} | ${{}}
|
||||
${'op'} | ${'identity'} | ${[1]} | ${0} | ${1}
|
||||
${'op'} | ${'identity'} | ${[1, 2]} | ${1} | ${'identity accepts up to 1 arguments, received 2 arguments'}
|
||||
${'op'} | ${'noop'} | ${[1, 2]} | ${0} | ${{}}
|
||||
${'op'} | ${'array'} | ${[1, 2, 3]} | ${0} | ${[1, 2, 3]}
|
||||
${'op'} | ${'array_length'} | ${[[1, 2, 3]]} | ${0} | ${3}
|
||||
${'op'} | ${'array_length'} | ${[]} | ${1} | ${'array_length accepts exactly one argument, found: 0'}
|
||||
${'op'} | ${'concat'} | ${[[1, 2], [3, 4], [5, 6]]} | ${0} | ${[1, 2, 3, 4, 5, 6]}
|
||||
${'op'} | ${'concat'} | ${[[1, 2]]} | ${0} | ${[1, 2]}
|
||||
${'op'} | ${'concat'} | ${[]} | ${0} | ${[]}
|
||||
${'op'} | ${'concat'} | ${[1, [1, 2], 1]} | ${1} | ${"All arguments of 'concat' must be arrays: arguments 0, 2 are not"}
|
||||
${'op'} | ${'string_to_b58'} | ${['test']} | ${0} | ${'3yZe7d'}
|
||||
${'op'} | ${'string_to_b58'} | ${['test', 1]} | ${1} | ${'string_to_b58 accepts only one string argument'}
|
||||
${'op'} | ${'string_from_b58'} | ${['3yZe7d']} | ${0} | ${'test'}
|
||||
${'op'} | ${'string_from_b58'} | ${['3yZe7d', 1]} | ${1} | ${'string_from_b58 accepts only one string argument'}
|
||||
${'op'} | ${'bytes_to_b58'} | ${[[116, 101, 115, 116]]} | ${0} | ${'3yZe7d'}
|
||||
${'op'} | ${'bytes_to_b58'} | ${[[116, 101, 115, 116], 1]} | ${1} | ${'bytes_to_b58 accepts only single argument: array of numbers'}
|
||||
${'op'} | ${'bytes_from_b58'} | ${['3yZe7d']} | ${0} | ${[116, 101, 115, 116]}
|
||||
${'op'} | ${'bytes_from_b58'} | ${['3yZe7d', 1]} | ${1} | ${'bytes_from_b58 accepts only one string argument'}
|
||||
${'op'} | ${'sha256_string'} | ${['hello, world!']} | ${0} | ${'QmVQ8pg6L1tpoWYeq6dpoWqnzZoSLCh7E96fCFXKvfKD3u'}
|
||||
${'op'} | ${'sha256_string'} | ${['hello, world!', true]} | ${0} | ${'84V7ZxLW7qKsx1Qvbd63BdGaHxUc3TfT2MBPqAXM7Wyu'}
|
||||
${'op'} | ${'sha256_string'} | ${[]} | ${1} | ${'sha256_string accepts 1-3 arguments, found: 0'}
|
||||
${'op'} | ${'concat_strings'} | ${[]} | ${0} | ${''}
|
||||
${'op'} | ${'concat_strings'} | ${['a', 'b', 'c']} | ${0} | ${'abc'}
|
||||
${'peer'} | ${'timeout'} | ${[200, []]} | ${0} | ${[]}
|
||||
${'peer'} | ${'timeout'} | ${[200, ['test']]} | ${0} | ${['test']}
|
||||
${'peer'} | ${'timeout'} | ${[]} | ${1} | ${'timeout accepts exactly two arguments: timeout duration in ms and a message string'}
|
||||
${'peer'} | ${'timeout'} | ${[200, 'test', 1]} | ${1} | ${'timeout accepts exactly two arguments: timeout duration in ms and a message string'}
|
||||
${'debug'} | ${'stringify'} | ${[]} | ${0} | ${'"<empty argument list>"'}
|
||||
${'debug'} | ${'stringify'} | ${[{ a: 10, b: 20 }]} | ${0} | ${a10b20}
|
||||
${'debug'} | ${'stringify'} | ${[1, 2, 3, 4]} | ${0} | ${oneTwoThreeFour}
|
||||
${'math'} | ${'add'} | ${[2, 2]} | ${0} | ${4}
|
||||
${'math'} | ${'add'} | ${[2]} | ${1} | ${'Expected 2 argument(s). Got 1'}
|
||||
${'math'} | ${'sub'} | ${[2, 2]} | ${0} | ${0}
|
||||
${'math'} | ${'sub'} | ${[2, 3]} | ${0} | ${-1}
|
||||
${'math'} | ${'mul'} | ${[2, 2]} | ${0} | ${4}
|
||||
${'math'} | ${'mul'} | ${[2, 0]} | ${0} | ${0}
|
||||
${'math'} | ${'mul'} | ${[2, -1]} | ${0} | ${-2}
|
||||
${'math'} | ${'fmul'} | ${[10, 0.66]} | ${0} | ${6}
|
||||
${'math'} | ${'fmul'} | ${[0.5, 0.5]} | ${0} | ${0}
|
||||
${'math'} | ${'fmul'} | ${[100.5, 0.5]} | ${0} | ${50}
|
||||
${'math'} | ${'div'} | ${[2, 2]} | ${0} | ${1}
|
||||
${'math'} | ${'div'} | ${[2, 3]} | ${0} | ${0}
|
||||
${'math'} | ${'div'} | ${[10, 5]} | ${0} | ${2}
|
||||
${'math'} | ${'rem'} | ${[10, 3]} | ${0} | ${1}
|
||||
${'math'} | ${'pow'} | ${[2, 2]} | ${0} | ${4}
|
||||
${'math'} | ${'pow'} | ${[2, 0]} | ${0} | ${1}
|
||||
${'math'} | ${'log'} | ${[2, 2]} | ${0} | ${1}
|
||||
${'math'} | ${'log'} | ${[2, 4]} | ${0} | ${2}
|
||||
${'cmp'} | ${'gt'} | ${[2, 4]} | ${0} | ${false}
|
||||
${'cmp'} | ${'gte'} | ${[2, 4]} | ${0} | ${false}
|
||||
${'cmp'} | ${'gte'} | ${[4, 2]} | ${0} | ${true}
|
||||
${'cmp'} | ${'gte'} | ${[2, 2]} | ${0} | ${true}
|
||||
${'cmp'} | ${'lt'} | ${[2, 4]} | ${0} | ${true}
|
||||
${'cmp'} | ${'lte'} | ${[2, 4]} | ${0} | ${true}
|
||||
${'cmp'} | ${'lte'} | ${[4, 2]} | ${0} | ${false}
|
||||
${'cmp'} | ${'lte'} | ${[2, 2]} | ${0} | ${true}
|
||||
${'cmp'} | ${'cmp'} | ${[2, 4]} | ${0} | ${-1}
|
||||
${'cmp'} | ${'cmp'} | ${[2, -4]} | ${0} | ${1}
|
||||
${'cmp'} | ${'cmp'} | ${[2, 2]} | ${0} | ${0}
|
||||
${'array'} | ${'sum'} | ${[[1, 2, 3]]} | ${0} | ${6}
|
||||
${'array'} | ${'dedup'} | ${[['a', 'a', 'b', 'c', 'a', 'b', 'c']]} | ${0} | ${['a', 'b', 'c']}
|
||||
${'array'} | ${'intersect'} | ${[['a', 'b', 'c'], ['c', 'b', 'd']]} | ${0} | ${['b', 'c']}
|
||||
${'array'} | ${'diff'} | ${[['a', 'b', 'c'], ['c', 'b', 'd']]} | ${0} | ${['a']}
|
||||
${'array'} | ${'sdiff'} | ${[['a', 'b', 'c'], ['c', 'b', 'd']]} | ${0} | ${['a', 'd']}
|
||||
${'json'} | ${'obj'} | ${['a', 10, 'b', 'string', 'c', null]} | ${0} | ${{ a: 10, b: 'string', c: null }}
|
||||
${'json'} | ${'obj'} | ${['a', 10, 'b', 'string', 'c']} | ${1} | ${'Expected even number of argument(s). Got 5'}
|
||||
${'json'} | ${'obj'} | ${[]} | ${0} | ${{}}
|
||||
${'json'} | ${'put'} | ${[{}, 'a', 10]} | ${0} | ${{ a: 10 }}
|
||||
${'json'} | ${'put'} | ${[{ b: 11 }, 'a', 10]} | ${0} | ${{ a: 10, b: 11 }}
|
||||
${'json'} | ${'put'} | ${['a', 'a', 11]} | ${1} | ${'Argument 0 expected to be of type object, Got string'}
|
||||
${'json'} | ${'put'} | ${[{}, 'a', 10, 'b', 20]} | ${1} | ${'Expected 3 argument(s). Got 5'}
|
||||
${'json'} | ${'put'} | ${[{}]} | ${1} | ${'Expected 3 argument(s). Got 1'}
|
||||
${'json'} | ${'puts'} | ${[{}, 'a', 10]} | ${0} | ${{ a: 10 }}
|
||||
${'json'} | ${'puts'} | ${[{ b: 11 }, 'a', 10]} | ${0} | ${{ a: 10, b: 11 }}
|
||||
${'json'} | ${'puts'} | ${[{}, 'a', 10, 'b', 'string', 'c', null]} | ${0} | ${{ a: 10, b: 'string', c: null }}
|
||||
${'json'} | ${'puts'} | ${[{ x: 'text' }, 'a', 10, 'b', 'string']} | ${0} | ${{ a: 10, b: 'string', x: 'text' }}
|
||||
${'json'} | ${'puts'} | ${[{}]} | ${1} | ${'Expected more than 3 argument(s). Got 1'}
|
||||
${'json'} | ${'puts'} | ${['a', 'a', 11]} | ${1} | ${'Argument 0 expected to be of type object, Got string'}
|
||||
${'json'} | ${'stringify'} | ${[{ a: 10, b: 'string', c: null }]} | ${0} | ${'{"a":10,"b":"string","c":null}'}
|
||||
${'json'} | ${'stringify'} | ${[1]} | ${1} | ${'Argument 0 expected to be of type object, Got number'}
|
||||
${'json'} | ${'parse'} | ${['{"a":10,"b":"string","c":null}']} | ${0} | ${{ a: 10, b: 'string', c: null }}
|
||||
${'json'} | ${'parse'} | ${['incorrect']} | ${1} | ${'Unexpected token i in JSON at position 0'}
|
||||
${'json'} | ${'parse'} | ${[10]} | ${1} | ${'Argument 0 expected to be of type string, Got number'}
|
||||
`(
|
||||
//
|
||||
'$fnName with $args expected retcode: $retCode and result: $result',
|
||||
async ({ serviceId, fnName, args, retCode, result }) => {
|
||||
// arrange
|
||||
const req: CallServiceData = {
|
||||
serviceId: serviceId,
|
||||
fnName: fnName,
|
||||
args: args,
|
||||
tetraplets: [],
|
||||
particleContext: {
|
||||
particleId: 'some',
|
||||
initPeerId: 'init peer id',
|
||||
timestamp: 595951200,
|
||||
ttl: 595961200,
|
||||
signature: 'sig',
|
||||
},
|
||||
};
|
||||
|
||||
// act
|
||||
const fn = builtInServices[req.serviceId][req.fnName];
|
||||
const res = await fn(req);
|
||||
|
||||
// assert
|
||||
expect(res).toMatchObject({
|
||||
retCode: retCode,
|
||||
result: result,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('should return correct error message for identiy service', async () => {
|
||||
// arrange
|
||||
const req: CallServiceData = {
|
||||
serviceId: 'peer',
|
||||
fnName: 'identify',
|
||||
args: [],
|
||||
tetraplets: [],
|
||||
particleContext: {
|
||||
particleId: 'some',
|
||||
initPeerId: 'init peer id',
|
||||
timestamp: 595951200,
|
||||
ttl: 595961200,
|
||||
signature: 'sig',
|
||||
},
|
||||
};
|
||||
|
||||
// act
|
||||
const fn = builtInServices[req.serviceId][req.fnName];
|
||||
const res = await fn(req);
|
||||
|
||||
// assert
|
||||
expect(res).toMatchObject({
|
||||
retCode: 0,
|
||||
result: {
|
||||
external_addresses: [],
|
||||
node_version: expect.stringContaining('js'),
|
||||
air_version: expect.stringContaining('js'),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const key = '+cmeYlZKj+MfSa9dpHV+BmLPm6wq4inGlsPlQ1GvtPk=';
|
||||
|
||||
const context = (async () => {
|
||||
const keyBytes = toUint8Array(key);
|
||||
const kp = await KeyPair.fromEd25519SK(keyBytes);
|
||||
const res = {
|
||||
peerKeyPair: kp,
|
||||
peerId: kp.getPeerId(),
|
||||
};
|
||||
return res;
|
||||
})();
|
||||
|
||||
const testData = [1, 2, 3, 4, 5, 6, 7, 9, 10];
|
||||
|
||||
// signature produced by KeyPair created from key above (`key` variable)
|
||||
const testDataSig = [
|
||||
224, 104, 245, 206, 140, 248, 27, 72, 68, 133, 111, 10, 164, 197, 242, 132, 107, 77, 224, 67, 99, 106, 76, 29, 144,
|
||||
121, 122, 169, 36, 173, 58, 80, 170, 102, 137, 253, 157, 247, 168, 87, 162, 223, 188, 214, 203, 220, 52, 246, 29,
|
||||
86, 77, 71, 224, 248, 16, 213, 254, 75, 78, 239, 243, 222, 241, 15,
|
||||
];
|
||||
|
||||
// signature produced by KeyPair created from some random KeyPair
|
||||
const testDataWrongSig = [
|
||||
116, 247, 189, 118, 236, 53, 147, 123, 219, 75, 176, 105, 101, 108, 233, 137, 97, 14, 146, 132, 252, 70, 51, 153,
|
||||
237, 167, 156, 150, 36, 90, 229, 108, 166, 231, 255, 137, 8, 246, 125, 0, 213, 150, 83, 196, 237, 221, 131, 159,
|
||||
157, 159, 25, 109, 95, 160, 181, 65, 254, 238, 47, 156, 240, 151, 58, 14,
|
||||
];
|
||||
|
||||
const makeTetraplet = (initPeerId: string, serviceId?: string, fnName?: string): CallParams<'data'> => {
|
||||
return {
|
||||
initPeerId: initPeerId,
|
||||
tetraplets: {
|
||||
data: [
|
||||
{
|
||||
function_name: fnName,
|
||||
service_id: serviceId,
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
};
|
||||
|
||||
describe('Sig service tests', () => {
|
||||
it('sig.sign should create the correct signature', async () => {
|
||||
const ctx = await context;
|
||||
const sig = new Sig(ctx.peerKeyPair);
|
||||
|
||||
const res = await sig.sign(testData, makeTetraplet(ctx.peerId));
|
||||
|
||||
expect(res.success).toBe(true);
|
||||
expect(res.signature).toStrictEqual(testDataSig);
|
||||
});
|
||||
|
||||
it('sig.verify should return true for the correct signature', async () => {
|
||||
const ctx = await context;
|
||||
const sig = new Sig(ctx.peerKeyPair);
|
||||
|
||||
const res = await sig.verify(testDataSig, testData);
|
||||
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
|
||||
it('sig.verify should return false for the incorrect signature', async () => {
|
||||
const ctx = await context;
|
||||
const sig = new Sig(ctx.peerKeyPair);
|
||||
|
||||
const res = await sig.verify(testDataWrongSig, testData);
|
||||
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
|
||||
it('sign-verify call chain should work', async () => {
|
||||
const ctx = await context;
|
||||
const sig = new Sig(ctx.peerKeyPair);
|
||||
|
||||
const signature = await sig.sign(testData, makeTetraplet(ctx.peerId));
|
||||
const res = await sig.verify(signature.signature as number[], testData);
|
||||
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
|
||||
it('sig.sign with defaultSigGuard should work for correct callParams', async () => {
|
||||
const ctx = await context;
|
||||
const sig = new Sig(ctx.peerKeyPair);
|
||||
sig.securityGuard = defaultSigGuard(ctx.peerId);
|
||||
|
||||
const signature = await sig.sign(testData, makeTetraplet(ctx.peerId, 'registry', 'get_route_bytes'));
|
||||
|
||||
await expect(signature).toBeDefined();
|
||||
});
|
||||
|
||||
it('sig.sign with defaultSigGuard should not allow particles initiated from incorrect service', async () => {
|
||||
const ctx = await context;
|
||||
const sig = new Sig(ctx.peerKeyPair);
|
||||
sig.securityGuard = defaultSigGuard(ctx.peerId);
|
||||
|
||||
const res = await sig.sign(testData, makeTetraplet(ctx.peerId, 'other_service', 'other_fn'));
|
||||
|
||||
await expect(res.success).toBe(false);
|
||||
await expect(res.error).toBe('Security guard validation failed');
|
||||
});
|
||||
|
||||
it('sig.sign with defaultSigGuard should not allow particles initiated from other peers', async () => {
|
||||
const ctx = await context;
|
||||
const sig = new Sig(ctx.peerKeyPair);
|
||||
sig.securityGuard = defaultSigGuard(ctx.peerId);
|
||||
|
||||
const res = await sig.sign(
|
||||
testData,
|
||||
makeTetraplet((await KeyPair.randomEd25519()).getPeerId(), 'registry', 'get_key_bytes'),
|
||||
);
|
||||
|
||||
await expect(res.success).toBe(false);
|
||||
await expect(res.error).toBe('Security guard validation failed');
|
||||
});
|
||||
|
||||
it('changing securityGuard should work', async () => {
|
||||
const ctx = await context;
|
||||
const sig = new Sig(ctx.peerKeyPair);
|
||||
sig.securityGuard = allowServiceFn('test', 'test');
|
||||
|
||||
const successful1 = await sig.sign(testData, makeTetraplet(ctx.peerId, 'test', 'test'));
|
||||
const unSuccessful1 = await sig.sign(testData, makeTetraplet(ctx.peerId, 'wrong', 'wrong'));
|
||||
|
||||
sig.securityGuard = allowServiceFn('wrong', 'wrong');
|
||||
|
||||
const successful2 = await sig.sign(testData, makeTetraplet(ctx.peerId, 'wrong', 'wrong'));
|
||||
const unSuccessful2 = await sig.sign(testData, makeTetraplet(ctx.peerId, 'test', 'test'));
|
||||
|
||||
expect(successful1.success).toBe(true);
|
||||
expect(successful2.success).toBe(true);
|
||||
expect(unSuccessful1.success).toBe(false);
|
||||
expect(unSuccessful2.success).toBe(false);
|
||||
});
|
||||
});
|
@ -0,0 +1,84 @@
|
||||
import { KeyPair } from '@fluencelabs/keypair';
|
||||
import { EphemeralNetwork, defaultConfig } from '../../ephemeral';
|
||||
import { ResultCodes } from '../../commonTypes';
|
||||
import { FluencePeer } from '../../FluencePeer';
|
||||
import { mkTestPeer } from '../util';
|
||||
|
||||
let en: EphemeralNetwork;
|
||||
let peer: FluencePeer;
|
||||
|
||||
// TODO: jest tests hang when running this test. Fix it (DXJ-219)
|
||||
describe.skip('Ephemeral networks tests', () => {
|
||||
beforeEach(async () => {
|
||||
en = new EphemeralNetwork(defaultConfig);
|
||||
await en.up();
|
||||
const relay = defaultConfig.peers[0].peerId;
|
||||
|
||||
peer = mkTestPeer();
|
||||
await peer.init({
|
||||
KeyPair: await KeyPair.randomEd25519(),
|
||||
});
|
||||
|
||||
const conn = en.getRelayConnection(relay, peer);
|
||||
await peer.connect(conn);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (peer) {
|
||||
await peer.stop();
|
||||
}
|
||||
if (en) {
|
||||
await en.down();
|
||||
}
|
||||
});
|
||||
|
||||
it('smoke test', async function () {
|
||||
const relay = peer.getStatus().relayPeerId!;
|
||||
|
||||
const peers = defaultConfig.peers.map((x) => x.peerId);
|
||||
|
||||
const script = `
|
||||
(seq
|
||||
(call "${relay}" ("op" "noop") [])
|
||||
(seq
|
||||
(call "${peers[0]}" ("op" "noop") [])
|
||||
(seq
|
||||
(call "${peers[1]}" ("op" "noop") [])
|
||||
(seq
|
||||
(call "${peers[2]}" ("op" "noop") [])
|
||||
(seq
|
||||
(call "${peers[3]}" ("op" "noop") [])
|
||||
(seq
|
||||
(call "${peers[4]}" ("op" "noop") [])
|
||||
(seq
|
||||
(call "${relay}" ("op" "noop") [])
|
||||
(call %init_peer_id% ("test" "test") [])
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`;
|
||||
|
||||
const particle = peer.internals.createNewParticle(script);
|
||||
if (particle instanceof Error) {
|
||||
throw particle;
|
||||
}
|
||||
|
||||
const promise = new Promise<string>((resolve) => {
|
||||
peer.internals.regHandler.forParticle(particle.id, 'test', 'test', (req) => {
|
||||
resolve('success');
|
||||
return {
|
||||
result: 'test',
|
||||
retCode: ResultCodes.success,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
peer.internals.initiateParticle(particle, () => {});
|
||||
|
||||
await expect(promise).resolves.toBe('success');
|
||||
});
|
||||
});
|
74
packages/core/js-peer/src/js-peer/__test__/util.ts
Normal file
74
packages/core/js-peer/src/js-peer/__test__/util.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import * as api from '@fluencelabs/aqua-api/aqua-api.js';
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import { FluencePeer, PeerConfig } from '../FluencePeer.js';
|
||||
import { Particle } from '../Particle.js';
|
||||
import { MakeServiceCall } from '../utils.js';
|
||||
import { avmModuleLoader, controlModuleLoader } from '../utilsForNode.js';
|
||||
import { ServiceDef } from '@fluencelabs/interfaces';
|
||||
import { callFunctionImpl } from '../../compilerSupport/callFunction.js';
|
||||
|
||||
import { marineLogFunction } from '../utils.js';
|
||||
import { MarineBackgroundRunner } from '../../marine/worker/index.js';
|
||||
import { MarineBasedAvmRunner } from '../avm.js';
|
||||
import { nodes } from './connection.js';
|
||||
import { WorkerLoaderFromFs } from '../../marine/deps-loader/node.js';
|
||||
|
||||
export const registerHandlersHelper = (
|
||||
peer: FluencePeer,
|
||||
particle: Particle,
|
||||
handlers: Record<string, Record<string, any>>,
|
||||
) => {
|
||||
Object.entries(handlers).forEach(([serviceId, service]) => {
|
||||
Object.entries(service).forEach(([fnName, fn]) => {
|
||||
peer.internals.regHandler.forParticle(particle.id, serviceId, fnName, MakeServiceCall(fn));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export type CompiledFnCall = (peer: FluencePeer, args: { [key: string]: any }) => Promise<unknown>;
|
||||
export type CompiledFile = {
|
||||
functions: { [key: string]: CompiledFnCall };
|
||||
services: { [key: string]: ServiceDef };
|
||||
};
|
||||
|
||||
export const compileAqua = async (aquaFile: string): Promise<CompiledFile> => {
|
||||
await fs.access(aquaFile);
|
||||
|
||||
const compilationResult = await api.Aqua.compile(new api.Path(aquaFile), [], undefined);
|
||||
|
||||
const functions = Object.entries(compilationResult.functions)
|
||||
.map(([name, fnInfo]) => {
|
||||
const callFn = (peer: FluencePeer, args: { [key: string]: any }) => {
|
||||
return callFunctionImpl(fnInfo.funcDef, fnInfo.script, {}, peer, args);
|
||||
};
|
||||
return { [name]: callFn };
|
||||
})
|
||||
.reduce((agg, obj) => {
|
||||
return { ...agg, ...obj };
|
||||
}, {});
|
||||
|
||||
return { functions, services: compilationResult.services };
|
||||
};
|
||||
|
||||
export const mkTestPeer = () => {
|
||||
const workerLoader = new WorkerLoaderFromFs('../../marine/worker-script');
|
||||
|
||||
const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader, marineLogFunction);
|
||||
const avm = new MarineBasedAvmRunner(marine, avmModuleLoader, undefined);
|
||||
return new FluencePeer(marine, avm);
|
||||
};
|
||||
|
||||
export const withPeer = async (action: (p: FluencePeer) => Promise<void>, config?: PeerConfig) => {
|
||||
const p = mkTestPeer();
|
||||
try {
|
||||
await p.start(config);
|
||||
await action(p);
|
||||
} finally {
|
||||
await p!.stop();
|
||||
}
|
||||
};
|
||||
|
||||
export const withConnectedPeer = async (action: (p: FluencePeer) => Promise<void>, config?: PeerConfig) => {
|
||||
return withPeer(action, { relay: nodes[0] });
|
||||
};
|
81
packages/core/js-peer/src/js-peer/_aqua/node-utils.ts
Normal file
81
packages/core/js-peer/src/js-peer/_aqua/node-utils.ts
Normal file
@ -0,0 +1,81 @@
|
||||
/**
|
||||
*
|
||||
* This file is auto-generated. Do not edit manually: changes may be erased.
|
||||
* Generated by Aqua compiler: https://github.com/fluencelabs/aqua/.
|
||||
* If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues
|
||||
* Aqua version: 0.7.7-362
|
||||
*
|
||||
*/
|
||||
import { CallParams } from '@fluencelabs/interfaces';
|
||||
import { registerServiceImpl } from '../../compilerSupport/registerService.js';
|
||||
import { FluencePeer } from '../FluencePeer.js';
|
||||
|
||||
// Services
|
||||
|
||||
export interface NodeUtilsDef {
|
||||
read_file: (
|
||||
path: string,
|
||||
callParams: CallParams<'path'>,
|
||||
) =>
|
||||
| { content: string | null; error: string | null; success: boolean }
|
||||
| Promise<{ content: string | null; error: string | null; success: boolean }>;
|
||||
}
|
||||
|
||||
export function registerNodeUtils(peer: FluencePeer, serviceId: string, service: any) {
|
||||
registerServiceImpl(
|
||||
peer,
|
||||
{
|
||||
defaultServiceId: 'node_utils',
|
||||
functions: {
|
||||
tag: 'labeledProduct',
|
||||
fields: {
|
||||
read_file: {
|
||||
tag: 'arrow',
|
||||
domain: {
|
||||
tag: 'labeledProduct',
|
||||
fields: {
|
||||
path: {
|
||||
tag: 'scalar',
|
||||
name: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
codomain: {
|
||||
tag: 'unlabeledProduct',
|
||||
items: [
|
||||
{
|
||||
tag: 'struct',
|
||||
name: 'ReadFileResult',
|
||||
fields: {
|
||||
content: {
|
||||
tag: 'option',
|
||||
type: {
|
||||
tag: 'scalar',
|
||||
name: 'string',
|
||||
},
|
||||
},
|
||||
error: {
|
||||
tag: 'option',
|
||||
type: {
|
||||
tag: 'scalar',
|
||||
name: 'string',
|
||||
},
|
||||
},
|
||||
success: {
|
||||
tag: 'scalar',
|
||||
name: 'bool',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
serviceId,
|
||||
service,
|
||||
);
|
||||
}
|
||||
|
||||
// Functions
|
139
packages/core/js-peer/src/js-peer/_aqua/services.ts
Normal file
139
packages/core/js-peer/src/js-peer/_aqua/services.ts
Normal file
@ -0,0 +1,139 @@
|
||||
/**
|
||||
*
|
||||
* This file is auto-generated. Do not edit manually: changes may be erased.
|
||||
* Generated by Aqua compiler: https://github.com/fluencelabs/aqua/.
|
||||
* If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues
|
||||
* Aqua version: 0.7.7-362
|
||||
*
|
||||
*/
|
||||
import { CallParams } from '@fluencelabs/interfaces';
|
||||
import { registerServiceImpl } from '../../compilerSupport/registerService.js';
|
||||
import { FluencePeer } from '../FluencePeer.js';
|
||||
|
||||
// Services
|
||||
|
||||
export interface SigDef {
|
||||
get_peer_id: (callParams: CallParams<null>) => string | Promise<string>;
|
||||
sign: (
|
||||
data: number[],
|
||||
callParams: CallParams<'data'>,
|
||||
) =>
|
||||
| { error: string | null; signature: number[] | null; success: boolean }
|
||||
| Promise<{ error: string | null; signature: number[] | null; success: boolean }>;
|
||||
verify: (
|
||||
signature: number[],
|
||||
data: number[],
|
||||
callParams: CallParams<'signature' | 'data'>,
|
||||
) => boolean | Promise<boolean>;
|
||||
}
|
||||
|
||||
export function registerSig(peer: FluencePeer, serviceId: string, service: any) {
|
||||
registerServiceImpl(
|
||||
peer,
|
||||
{
|
||||
defaultServiceId: 'sig',
|
||||
functions: {
|
||||
tag: 'labeledProduct',
|
||||
fields: {
|
||||
get_peer_id: {
|
||||
tag: 'arrow',
|
||||
domain: {
|
||||
tag: 'nil',
|
||||
},
|
||||
codomain: {
|
||||
tag: 'unlabeledProduct',
|
||||
items: [
|
||||
{
|
||||
tag: 'scalar',
|
||||
name: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
sign: {
|
||||
tag: 'arrow',
|
||||
domain: {
|
||||
tag: 'labeledProduct',
|
||||
fields: {
|
||||
data: {
|
||||
tag: 'array',
|
||||
type: {
|
||||
tag: 'scalar',
|
||||
name: 'u8',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
codomain: {
|
||||
tag: 'unlabeledProduct',
|
||||
items: [
|
||||
{
|
||||
tag: 'struct',
|
||||
name: 'SignResult',
|
||||
fields: {
|
||||
error: {
|
||||
tag: 'option',
|
||||
type: {
|
||||
tag: 'scalar',
|
||||
name: 'string',
|
||||
},
|
||||
},
|
||||
signature: {
|
||||
tag: 'option',
|
||||
type: {
|
||||
tag: 'array',
|
||||
type: {
|
||||
tag: 'scalar',
|
||||
name: 'u8',
|
||||
},
|
||||
},
|
||||
},
|
||||
success: {
|
||||
tag: 'scalar',
|
||||
name: 'bool',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
verify: {
|
||||
tag: 'arrow',
|
||||
domain: {
|
||||
tag: 'labeledProduct',
|
||||
fields: {
|
||||
signature: {
|
||||
tag: 'array',
|
||||
type: {
|
||||
tag: 'scalar',
|
||||
name: 'u8',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
tag: 'array',
|
||||
type: {
|
||||
tag: 'scalar',
|
||||
name: 'u8',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
codomain: {
|
||||
tag: 'unlabeledProduct',
|
||||
items: [
|
||||
{
|
||||
tag: 'scalar',
|
||||
name: 'bool',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
serviceId,
|
||||
service,
|
||||
);
|
||||
}
|
||||
|
||||
// Functions
|
138
packages/core/js-peer/src/js-peer/_aqua/single-module-srv.ts
Normal file
138
packages/core/js-peer/src/js-peer/_aqua/single-module-srv.ts
Normal file
@ -0,0 +1,138 @@
|
||||
/**
|
||||
*
|
||||
* This file is auto-generated. Do not edit manually: changes may be erased.
|
||||
* Generated by Aqua compiler: https://github.com/fluencelabs/aqua/.
|
||||
* If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues
|
||||
* Aqua version: 0.7.7-362
|
||||
*
|
||||
*/
|
||||
import { CallParams } from '@fluencelabs/interfaces';
|
||||
import { registerServiceImpl } from '../../compilerSupport/registerService.js';
|
||||
import { FluencePeer } from '../FluencePeer.js';
|
||||
|
||||
// Services
|
||||
|
||||
export interface SrvDef {
|
||||
create: (
|
||||
wasm_b64_content: string,
|
||||
callParams: CallParams<'wasm_b64_content'>,
|
||||
) =>
|
||||
| { error: string | null; service_id: string | null; success: boolean }
|
||||
| Promise<{ error: string | null; service_id: string | null; success: boolean }>;
|
||||
list: (callParams: CallParams<null>) => string[] | Promise<string[]>;
|
||||
remove: (
|
||||
service_id: string,
|
||||
callParams: CallParams<'service_id'>,
|
||||
) => { error: string | null; success: boolean } | Promise<{ error: string | null; success: boolean }>;
|
||||
}
|
||||
|
||||
export function registerSrv(peer: FluencePeer, serviceId: string, service: any) {
|
||||
registerServiceImpl(
|
||||
peer,
|
||||
{
|
||||
defaultServiceId: 'single_module_srv',
|
||||
functions: {
|
||||
tag: 'labeledProduct',
|
||||
fields: {
|
||||
create: {
|
||||
tag: 'arrow',
|
||||
domain: {
|
||||
tag: 'labeledProduct',
|
||||
fields: {
|
||||
wasm_b64_content: {
|
||||
tag: 'scalar',
|
||||
name: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
codomain: {
|
||||
tag: 'unlabeledProduct',
|
||||
items: [
|
||||
{
|
||||
tag: 'struct',
|
||||
name: 'ServiceCreationResult',
|
||||
fields: {
|
||||
error: {
|
||||
tag: 'option',
|
||||
type: {
|
||||
tag: 'scalar',
|
||||
name: 'string',
|
||||
},
|
||||
},
|
||||
service_id: {
|
||||
tag: 'option',
|
||||
type: {
|
||||
tag: 'scalar',
|
||||
name: 'string',
|
||||
},
|
||||
},
|
||||
success: {
|
||||
tag: 'scalar',
|
||||
name: 'bool',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
list: {
|
||||
tag: 'arrow',
|
||||
domain: {
|
||||
tag: 'nil',
|
||||
},
|
||||
codomain: {
|
||||
tag: 'unlabeledProduct',
|
||||
items: [
|
||||
{
|
||||
tag: 'array',
|
||||
type: {
|
||||
tag: 'scalar',
|
||||
name: 'string',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
remove: {
|
||||
tag: 'arrow',
|
||||
domain: {
|
||||
tag: 'labeledProduct',
|
||||
fields: {
|
||||
service_id: {
|
||||
tag: 'scalar',
|
||||
name: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
codomain: {
|
||||
tag: 'unlabeledProduct',
|
||||
items: [
|
||||
{
|
||||
tag: 'struct',
|
||||
name: 'RemoveResult',
|
||||
fields: {
|
||||
error: {
|
||||
tag: 'option',
|
||||
type: {
|
||||
tag: 'scalar',
|
||||
name: 'string',
|
||||
},
|
||||
},
|
||||
success: {
|
||||
tag: 'scalar',
|
||||
name: 'bool',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
serviceId,
|
||||
service,
|
||||
);
|
||||
}
|
||||
|
||||
// Functions
|
36
packages/core/js-peer/src/js-peer/avm.ts
Normal file
36
packages/core/js-peer/src/js-peer/avm.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import type { CallResultsArray, InterpreterResult, RunParameters } from '@fluencelabs/avm';
|
||||
import { deserializeAvmResult, serializeAvmArgs } from '@fluencelabs/avm';
|
||||
import type { LogLevel } from '@fluencelabs/marine-js/dist/types';
|
||||
import type { IMarine, IAvmRunner, IWasmLoader } from '../interfaces/index.js';
|
||||
|
||||
export class MarineBasedAvmRunner implements IAvmRunner {
|
||||
constructor(private marine: IMarine, private avmWasmLoader: IWasmLoader, private logLevel: LogLevel | undefined) {}
|
||||
|
||||
async run(
|
||||
runParams: RunParameters,
|
||||
air: string,
|
||||
prevData: Uint8Array,
|
||||
data: Uint8Array,
|
||||
callResults: CallResultsArray,
|
||||
): Promise<InterpreterResult | Error> {
|
||||
const args = serializeAvmArgs(runParams, air, prevData, data, callResults);
|
||||
|
||||
let avmCallResult: InterpreterResult | Error;
|
||||
try {
|
||||
const res = await this.marine.callService('avm', 'invoke', args, undefined);
|
||||
avmCallResult = deserializeAvmResult(res);
|
||||
} catch (e) {
|
||||
avmCallResult = e instanceof Error ? e : new Error((e as any).toString());
|
||||
}
|
||||
|
||||
return avmCallResult;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
await this.marine.start();
|
||||
await this.avmWasmLoader.start();
|
||||
await this.marine.createService(this.avmWasmLoader.getValue(), 'avm', this.logLevel);
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {}
|
||||
}
|
71
packages/core/js-peer/src/js-peer/builtins/Sig.ts
Normal file
71
packages/core/js-peer/src/js-peer/builtins/Sig.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { CallParams, PeerIdB58 } from '@fluencelabs/interfaces';
|
||||
import { KeyPair } from '../../keypair/index.js';
|
||||
import { SigDef } from '../_aqua/services.js';
|
||||
import { allowOnlyParticleOriginatedAt, allowServiceFn, and, or, SecurityGuard } from './securityGuard.js';
|
||||
|
||||
export const defaultSigGuard = (peerId: PeerIdB58) => {
|
||||
return and<'data'>(
|
||||
allowOnlyParticleOriginatedAt(peerId),
|
||||
or(
|
||||
allowServiceFn('trust-graph', 'get_trust_bytes'),
|
||||
allowServiceFn('trust-graph', 'get_revocation_bytes'),
|
||||
allowServiceFn('registry', 'get_key_bytes'),
|
||||
allowServiceFn('registry', 'get_record_bytes'),
|
||||
allowServiceFn('registry', 'get_record_metadata_bytes'),
|
||||
allowServiceFn('registry', 'get_tombstone_bytes'),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export class Sig implements SigDef {
|
||||
private _keyPair: KeyPair;
|
||||
|
||||
constructor(keyPair: KeyPair) {
|
||||
this._keyPair = keyPair;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configurable security guard for sign method
|
||||
*/
|
||||
securityGuard: SecurityGuard<'data'> = (params) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the public key of KeyPair. Required by aqua
|
||||
*/
|
||||
get_peer_id() {
|
||||
return this._keyPair.getPeerId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the data using key pair's private key. Required by aqua
|
||||
*/
|
||||
async sign(
|
||||
data: number[],
|
||||
callParams: CallParams<'data'>,
|
||||
): Promise<{ error: string | null; signature: number[] | null; success: boolean }> {
|
||||
if (!this.securityGuard(callParams)) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Security guard validation failed',
|
||||
signature: null,
|
||||
};
|
||||
}
|
||||
|
||||
const signedData = await this._keyPair.signBytes(Uint8Array.from(data));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
error: null,
|
||||
signature: Array.from(signedData),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the signature. Required by aqua
|
||||
*/
|
||||
verify(signature: number[], data: number[]): Promise<boolean> {
|
||||
return this._keyPair.verify(Uint8Array.from(data), Uint8Array.from(signature));
|
||||
}
|
||||
}
|
131
packages/core/js-peer/src/js-peer/builtins/SingleModuleSrv.ts
Normal file
131
packages/core/js-peer/src/js-peer/builtins/SingleModuleSrv.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { SrvDef } from '../_aqua/single-module-srv.js';
|
||||
import { NodeUtilsDef } from '../_aqua/node-utils.js';
|
||||
import { FluencePeer } from '../FluencePeer.js';
|
||||
import { CallParams } from '@fluencelabs/interfaces';
|
||||
import { Buffer } from 'buffer';
|
||||
import { allowOnlyParticleOriginatedAt, SecurityGuard } from './securityGuard.js';
|
||||
|
||||
export const defaultGuard = (peer: FluencePeer) => {
|
||||
return allowOnlyParticleOriginatedAt<any>(peer.getStatus().peerId!);
|
||||
};
|
||||
|
||||
export class Srv implements SrvDef {
|
||||
private services: Set<string> = new Set();
|
||||
|
||||
constructor(private peer: FluencePeer) {
|
||||
this.securityGuard_create = defaultGuard(this.peer);
|
||||
this.securityGuard_remove = defaultGuard(this.peer);
|
||||
}
|
||||
|
||||
securityGuard_create: SecurityGuard<'wasm_b64_content'>;
|
||||
|
||||
async create(wasm_b64_content: string, callParams: CallParams<'wasm_b64_content'>) {
|
||||
if (!this.securityGuard_create(callParams)) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Security guard validation failed',
|
||||
service_id: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const newServiceId = uuidv4();
|
||||
const buffer = Buffer.from(wasm_b64_content, 'base64');
|
||||
const sab = new SharedArrayBuffer(buffer.length);
|
||||
const tmp = new Uint8Array(sab);
|
||||
tmp.set(buffer, 0);
|
||||
await this.peer.registerMarineService(sab, newServiceId);
|
||||
this.services.add(newServiceId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
service_id: newServiceId,
|
||||
error: null,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
success: true,
|
||||
service_id: null,
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
securityGuard_remove: SecurityGuard<'service_id'>;
|
||||
|
||||
remove(service_id: string, callParams: CallParams<'service_id'>) {
|
||||
if (!this.securityGuard_remove(callParams)) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Security guard validation failed',
|
||||
service_id: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.services.has(service_id)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Service with id ${service_id} not found`,
|
||||
};
|
||||
}
|
||||
|
||||
this.peer.removeMarineService(service_id);
|
||||
this.services.delete(service_id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
list() {
|
||||
return Array.from(this.services.values());
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeUtils implements NodeUtilsDef {
|
||||
constructor(private peer: FluencePeer) {
|
||||
this.securityGuard_readFile = defaultGuard(this.peer);
|
||||
}
|
||||
|
||||
securityGuard_readFile: SecurityGuard<'path'>;
|
||||
|
||||
async read_file(path: string, callParams: CallParams<'path'>) {
|
||||
// TODO: split node-only and universal services into different client packages
|
||||
// if (!isNode) {
|
||||
// return {
|
||||
// success: false,
|
||||
// error: 'read_file is only supported in node.js',
|
||||
// content: null,
|
||||
// };
|
||||
// }
|
||||
|
||||
if (!this.securityGuard_readFile(callParams)) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Security guard validation failed',
|
||||
content: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// eval('require') is needed so that
|
||||
// webpack will complain about missing dependencies for web target
|
||||
const r = eval('require');
|
||||
const fs = r('fs').promises;
|
||||
const data = await fs.readFile(path);
|
||||
return {
|
||||
success: true,
|
||||
content: data,
|
||||
error: null,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: err.message,
|
||||
content: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
605
packages/core/js-peer/src/js-peer/builtins/common.ts
Normal file
605
packages/core/js-peer/src/js-peer/builtins/common.ts
Normal file
@ -0,0 +1,605 @@
|
||||
/*
|
||||
* Copyright 2021 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as bs58 from 'bs58';
|
||||
|
||||
import { sha256 } from 'multiformats/hashes/sha2';
|
||||
import { CallServiceResult } from '@fluencelabs/avm';
|
||||
|
||||
import { GenericCallServiceHandler, ResultCodes } from '../../interfaces/commonTypes.js';
|
||||
import { jsonify } from '../utils.js';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
//@ts-ignore
|
||||
const { encode, decode } = bs58.default;
|
||||
|
||||
const success = (result: any): CallServiceResult => {
|
||||
return {
|
||||
result: result,
|
||||
retCode: ResultCodes.success,
|
||||
};
|
||||
};
|
||||
|
||||
const error = (error: string): CallServiceResult => {
|
||||
return {
|
||||
result: error,
|
||||
retCode: ResultCodes.error,
|
||||
};
|
||||
};
|
||||
|
||||
const errorNotImpl = (methodName: string) => {
|
||||
return error(`The JS implementation of Peer does not support "${methodName}"`);
|
||||
};
|
||||
|
||||
const makeJsonImpl = (args: Array<any>) => {
|
||||
const [obj, ...kvs] = args;
|
||||
|
||||
const toMerge: Record<string, any> = {};
|
||||
for (let i = 0; i < kvs.length / 2; i++) {
|
||||
const k = kvs[i * 2];
|
||||
if (!isString(k)) {
|
||||
return error(`Argument ${k} is expected to be string`);
|
||||
}
|
||||
const v = kvs[i * 2 + 1];
|
||||
toMerge[k] = v;
|
||||
}
|
||||
|
||||
const res = { ...obj, ...toMerge };
|
||||
return success(res);
|
||||
};
|
||||
|
||||
export const builtInServices: Record<string, Record<string, GenericCallServiceHandler>> = {
|
||||
peer: {
|
||||
identify: () => {
|
||||
return success({
|
||||
external_addresses: [],
|
||||
// TODO: remove hardcoded values
|
||||
node_version: 'js-0.23.0',
|
||||
air_version: 'js-0.24.2',
|
||||
});
|
||||
},
|
||||
|
||||
timestamp_ms: () => {
|
||||
return success(Date.now());
|
||||
},
|
||||
|
||||
timestamp_sec: () => {
|
||||
return success(Math.floor(Date.now() / 1000));
|
||||
},
|
||||
|
||||
is_connected: () => {
|
||||
return errorNotImpl('peer.is_connected');
|
||||
},
|
||||
|
||||
connect: () => {
|
||||
return errorNotImpl('peer.connect');
|
||||
},
|
||||
|
||||
get_contact: () => {
|
||||
return errorNotImpl('peer.get_contact');
|
||||
},
|
||||
|
||||
timeout: (req) => {
|
||||
if (req.args.length !== 2) {
|
||||
return error('timeout accepts exactly two arguments: timeout duration in ms and a message string');
|
||||
}
|
||||
const durationMs = req.args[0];
|
||||
const message = req.args[1];
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const res = success(message);
|
||||
resolve(res);
|
||||
}, durationMs);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
kad: {
|
||||
neighborhood: () => {
|
||||
return errorNotImpl('kad.neighborhood');
|
||||
},
|
||||
|
||||
merge: () => {
|
||||
return errorNotImpl('kad.merge');
|
||||
},
|
||||
},
|
||||
|
||||
srv: {
|
||||
list: () => {
|
||||
return errorNotImpl('srv.list');
|
||||
},
|
||||
|
||||
create: () => {
|
||||
return errorNotImpl('srv.create');
|
||||
},
|
||||
|
||||
get_interface: () => {
|
||||
return errorNotImpl('srv.get_interface');
|
||||
},
|
||||
|
||||
resolve_alias: () => {
|
||||
return errorNotImpl('srv.resolve_alias');
|
||||
},
|
||||
|
||||
add_alias: () => {
|
||||
return errorNotImpl('srv.add_alias');
|
||||
},
|
||||
|
||||
remove: () => {
|
||||
return errorNotImpl('srv.remove');
|
||||
},
|
||||
},
|
||||
|
||||
dist: {
|
||||
add_module_from_vault: () => {
|
||||
return errorNotImpl('dist.add_module_from_vault');
|
||||
},
|
||||
|
||||
add_module: () => {
|
||||
return errorNotImpl('dist.add_module');
|
||||
},
|
||||
|
||||
add_blueprint: () => {
|
||||
return errorNotImpl('dist.add_blueprint');
|
||||
},
|
||||
|
||||
make_module_config: () => {
|
||||
return errorNotImpl('dist.make_module_config');
|
||||
},
|
||||
|
||||
load_module_config: () => {
|
||||
return errorNotImpl('dist.load_module_config');
|
||||
},
|
||||
|
||||
default_module_config: () => {
|
||||
return errorNotImpl('dist.default_module_config');
|
||||
},
|
||||
|
||||
make_blueprint: () => {
|
||||
return errorNotImpl('dist.make_blueprint');
|
||||
},
|
||||
|
||||
load_blueprint: () => {
|
||||
return errorNotImpl('dist.load_blueprint');
|
||||
},
|
||||
|
||||
list_modules: () => {
|
||||
return errorNotImpl('dist.list_modules');
|
||||
},
|
||||
|
||||
get_module_interface: () => {
|
||||
return errorNotImpl('dist.get_module_interface');
|
||||
},
|
||||
|
||||
list_blueprints: () => {
|
||||
return errorNotImpl('dist.list_blueprints');
|
||||
},
|
||||
},
|
||||
|
||||
script: {
|
||||
add: () => {
|
||||
return errorNotImpl('script.add');
|
||||
},
|
||||
|
||||
remove: () => {
|
||||
return errorNotImpl('script.remove');
|
||||
},
|
||||
|
||||
list: () => {
|
||||
return errorNotImpl('script.list');
|
||||
},
|
||||
},
|
||||
|
||||
op: {
|
||||
noop: () => {
|
||||
return success({});
|
||||
},
|
||||
|
||||
array: (req) => {
|
||||
return success(req.args);
|
||||
},
|
||||
|
||||
array_length: (req) => {
|
||||
if (req.args.length !== 1) {
|
||||
return error('array_length accepts exactly one argument, found: ' + req.args.length);
|
||||
} else {
|
||||
return success(req.args[0].length);
|
||||
}
|
||||
},
|
||||
|
||||
identity: (req) => {
|
||||
if (req.args.length > 1) {
|
||||
return error(`identity accepts up to 1 arguments, received ${req.args.length} arguments`);
|
||||
} else {
|
||||
return success(req.args.length === 0 ? {} : req.args[0]);
|
||||
}
|
||||
},
|
||||
|
||||
concat: (req) => {
|
||||
const incorrectArgIndices = req.args //
|
||||
.map((x, i) => [Array.isArray(x), i])
|
||||
.filter(([isArray, _]) => !isArray)
|
||||
.map(([_, index]) => index);
|
||||
|
||||
if (incorrectArgIndices.length > 0) {
|
||||
const str = incorrectArgIndices.join(', ');
|
||||
return error(`All arguments of 'concat' must be arrays: arguments ${str} are not`);
|
||||
} else {
|
||||
return success([].concat.apply([], req.args));
|
||||
}
|
||||
},
|
||||
|
||||
string_to_b58: (req) => {
|
||||
if (req.args.length !== 1) {
|
||||
return error('string_to_b58 accepts only one string argument');
|
||||
} else {
|
||||
return success(encode(new TextEncoder().encode(req.args[0])));
|
||||
}
|
||||
},
|
||||
|
||||
string_from_b58: (req) => {
|
||||
if (req.args.length !== 1) {
|
||||
return error('string_from_b58 accepts only one string argument');
|
||||
} else {
|
||||
return success(new TextDecoder().decode(decode(req.args[0])));
|
||||
}
|
||||
},
|
||||
|
||||
bytes_to_b58: (req) => {
|
||||
if (req.args.length !== 1 || !Array.isArray(req.args[0])) {
|
||||
return error('bytes_to_b58 accepts only single argument: array of numbers');
|
||||
} else {
|
||||
const argumentArray = req.args[0] as number[];
|
||||
return success(encode(new Uint8Array(argumentArray)));
|
||||
}
|
||||
},
|
||||
|
||||
bytes_from_b58: (req) => {
|
||||
if (req.args.length !== 1) {
|
||||
return error('bytes_from_b58 accepts only one string argument');
|
||||
} else {
|
||||
return success(Array.from(decode(req.args[0])));
|
||||
}
|
||||
},
|
||||
|
||||
sha256_string: async (req) => {
|
||||
if (req.args.length < 1 || req.args.length > 3) {
|
||||
return error(`sha256_string accepts 1-3 arguments, found: ${req.args.length}`);
|
||||
} else {
|
||||
const [input, digestOnly, asBytes] = req.args;
|
||||
const inBuffer = Buffer.from(input);
|
||||
const multihash = await sha256.digest(inBuffer);
|
||||
|
||||
const outBytes = digestOnly ? multihash.digest : multihash.bytes;
|
||||
const res = asBytes ? Array.from(outBytes) : encode(outBytes);
|
||||
|
||||
return success(res);
|
||||
}
|
||||
},
|
||||
|
||||
concat_strings: (req) => {
|
||||
const res = ''.concat(...req.args);
|
||||
return success(res);
|
||||
},
|
||||
},
|
||||
|
||||
debug: {
|
||||
stringify: (req) => {
|
||||
let out;
|
||||
|
||||
if (req.args.length === 0) {
|
||||
out = '<empty argument list>';
|
||||
} else if (req.args.length === 1) {
|
||||
out = req.args[0];
|
||||
} else {
|
||||
out = req.args;
|
||||
}
|
||||
|
||||
return success(jsonify(out));
|
||||
},
|
||||
},
|
||||
|
||||
math: {
|
||||
add: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [x, y] = req.args;
|
||||
return success(x + y);
|
||||
},
|
||||
|
||||
sub: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [x, y] = req.args;
|
||||
return success(x - y);
|
||||
},
|
||||
|
||||
mul: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [x, y] = req.args;
|
||||
return success(x * y);
|
||||
},
|
||||
|
||||
fmul: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [x, y] = req.args;
|
||||
return success(Math.floor(x * y));
|
||||
},
|
||||
|
||||
div: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [x, y] = req.args;
|
||||
return success(Math.floor(x / y));
|
||||
},
|
||||
|
||||
rem: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [x, y] = req.args;
|
||||
return success(x % y);
|
||||
},
|
||||
|
||||
pow: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [x, y] = req.args;
|
||||
return success(Math.pow(x, y));
|
||||
},
|
||||
|
||||
log: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [x, y] = req.args;
|
||||
return success(Math.log(y) / Math.log(x));
|
||||
},
|
||||
},
|
||||
|
||||
cmp: {
|
||||
gt: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [x, y] = req.args;
|
||||
return success(x > y);
|
||||
},
|
||||
|
||||
gte: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [x, y] = req.args;
|
||||
return success(x >= y);
|
||||
},
|
||||
|
||||
lt: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [x, y] = req.args;
|
||||
return success(x < y);
|
||||
},
|
||||
|
||||
lte: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [x, y] = req.args;
|
||||
return success(x <= y);
|
||||
},
|
||||
|
||||
cmp: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [x, y] = req.args;
|
||||
return success(x === y ? 0 : x > y ? 1 : -1);
|
||||
},
|
||||
},
|
||||
|
||||
array: {
|
||||
sum: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 1))) {
|
||||
return err;
|
||||
}
|
||||
const [xs] = req.args;
|
||||
return success(xs.reduce((agg: any, cur: any) => agg + cur, 0));
|
||||
},
|
||||
|
||||
dedup: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 1))) {
|
||||
return err;
|
||||
}
|
||||
const [xs] = req.args;
|
||||
const set = new Set(xs);
|
||||
return success(Array.from(set));
|
||||
},
|
||||
|
||||
intersect: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [xs, ys] = req.args;
|
||||
const intersection = xs.filter((x: any) => ys.includes(x));
|
||||
return success(intersection);
|
||||
},
|
||||
|
||||
diff: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [xs, ys] = req.args;
|
||||
const diff = xs.filter((x: unknown) => !ys.includes(x));
|
||||
return success(diff);
|
||||
},
|
||||
|
||||
sdiff: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 2))) {
|
||||
return err;
|
||||
}
|
||||
const [xs, ys] = req.args;
|
||||
const sdiff = [
|
||||
// force new line
|
||||
...xs.filter((y: unknown) => !ys.includes(y)),
|
||||
...ys.filter((x: unknown) => !xs.includes(x)),
|
||||
];
|
||||
return success(sdiff);
|
||||
},
|
||||
},
|
||||
|
||||
json: {
|
||||
obj: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCountEven(req, 1))) {
|
||||
return err;
|
||||
}
|
||||
|
||||
return makeJsonImpl([{}, ...req.args]);
|
||||
},
|
||||
|
||||
put: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 3))) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if ((err = checkForArgumentType(req, 0, 'object'))) {
|
||||
return err;
|
||||
}
|
||||
|
||||
return makeJsonImpl(req.args);
|
||||
},
|
||||
|
||||
puts: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCountOdd(req, 1))) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if ((err = checkForArgumentsCountMoreThan(req, 3))) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if ((err = checkForArgumentType(req, 0, 'object'))) {
|
||||
return err;
|
||||
}
|
||||
|
||||
return makeJsonImpl(req.args);
|
||||
},
|
||||
|
||||
stringify: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 1))) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if ((err = checkForArgumentType(req, 0, 'object'))) {
|
||||
return err;
|
||||
}
|
||||
|
||||
const [json] = req.args;
|
||||
const res = JSON.stringify(json);
|
||||
return success(res);
|
||||
},
|
||||
|
||||
parse: (req) => {
|
||||
let err;
|
||||
if ((err = checkForArgumentsCount(req, 1))) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if ((err = checkForArgumentType(req, 0, 'string'))) {
|
||||
return err;
|
||||
}
|
||||
|
||||
const [raw] = req.args;
|
||||
try {
|
||||
const json = JSON.parse(raw);
|
||||
return success(json);
|
||||
} catch (err: any) {
|
||||
return error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const checkForArgumentsCount = (req: { args: Array<unknown> }, count: number) => {
|
||||
if (req.args.length !== count) {
|
||||
return error(`Expected ${count} argument(s). Got ${req.args.length}`);
|
||||
}
|
||||
};
|
||||
|
||||
const checkForArgumentsCountMoreThan = (req: { args: Array<unknown> }, count: number) => {
|
||||
if (req.args.length < count) {
|
||||
return error(`Expected more than ${count} argument(s). Got ${req.args.length}`);
|
||||
}
|
||||
};
|
||||
|
||||
const checkForArgumentsCountEven = (req: { args: Array<unknown> }, count: number) => {
|
||||
if (req.args.length % 2 === 1) {
|
||||
return error(`Expected even number of argument(s). Got ${req.args.length}`);
|
||||
}
|
||||
};
|
||||
|
||||
const checkForArgumentsCountOdd = (req: { args: Array<unknown> }, count: number) => {
|
||||
if (req.args.length % 2 === 0) {
|
||||
return error(`Expected odd number of argument(s). Got ${req.args.length}`);
|
||||
}
|
||||
};
|
||||
|
||||
const checkForArgumentType = (req: { args: Array<unknown> }, index: number, type: string) => {
|
||||
const actual = typeof req.args[index];
|
||||
if (actual !== type) {
|
||||
return error(`Argument ${index} expected to be of type ${type}, Got ${actual}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const isString = (unknown: unknown): unknown is string => {
|
||||
return unknown !== null && typeof unknown === 'string';
|
||||
};
|
||||
|
||||
export const isObject = (unknown: unknown): unknown is object => {
|
||||
return unknown !== null && typeof unknown === 'object';
|
||||
};
|
64
packages/core/js-peer/src/js-peer/builtins/securityGuard.ts
Normal file
64
packages/core/js-peer/src/js-peer/builtins/securityGuard.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { SecurityTetraplet } from '@fluencelabs/avm';
|
||||
import { CallParams, PeerIdB58 } from '@fluencelabs/interfaces';
|
||||
|
||||
type ArgName = string | null;
|
||||
|
||||
/**
|
||||
* A predicate of call params for sig service's sign method which determines whether signing operation is allowed or not
|
||||
*/
|
||||
export type SecurityGuard<T extends ArgName> = (params: CallParams<T>) => boolean;
|
||||
|
||||
/**
|
||||
* Only allow calls when tetraplet for 'data' argument satisfies the predicate
|
||||
*/
|
||||
export const allowTetraplet = <T extends ArgName>(
|
||||
pred: (tetraplet: SecurityTetraplet) => boolean,
|
||||
): SecurityGuard<T> => {
|
||||
return (params) => {
|
||||
const t = params.tetraplets.data[0];
|
||||
return pred(t);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Only allow data which comes from the specified serviceId and fnName
|
||||
*/
|
||||
export const allowServiceFn = <T extends ArgName>(serviceId: string, fnName: string): SecurityGuard<T> => {
|
||||
return allowTetraplet((t) => {
|
||||
return t.service_id === serviceId && t.function_name === fnName;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Only allow data originated from the specified json_path
|
||||
*/
|
||||
export const allowExactJsonPath = <T extends ArgName>(jsonPath: string): SecurityGuard<T> => {
|
||||
return allowTetraplet((t) => {
|
||||
return t.json_path === jsonPath;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Only allow signing when particle is initiated at the specified peer
|
||||
*/
|
||||
export const allowOnlyParticleOriginatedAt = <T extends ArgName>(peerId: PeerIdB58): SecurityGuard<T> => {
|
||||
return (params) => {
|
||||
return params.initPeerId === peerId;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Only allow signing when all of the predicates are satisfied.
|
||||
* Useful for predicates reuse
|
||||
*/
|
||||
export const and = <T extends ArgName>(...predicates: SecurityGuard<T>[]): SecurityGuard<T> => {
|
||||
return (params) => predicates.every((x) => x(params));
|
||||
};
|
||||
|
||||
/**
|
||||
* Only allow signing when any of the predicates are satisfied.
|
||||
* Useful for predicates reuse
|
||||
*/
|
||||
export const or = <T extends ArgName>(...predicates: SecurityGuard<T>[]): SecurityGuard<T> => {
|
||||
return (params) => predicates.some((x) => x(params));
|
||||
};
|
251
packages/core/js-peer/src/js-peer/ephemeral.ts
Normal file
251
packages/core/js-peer/src/js-peer/ephemeral.ts
Normal file
@ -0,0 +1,251 @@
|
||||
import { PeerIdB58 } from '@fluencelabs/interfaces';
|
||||
import { FluenceConnection, ParticleHandler } from '../interfaces/index.js';
|
||||
import { fromBase64Sk } from '../keypair/index.js';
|
||||
import { FluencePeer } from './FluencePeer.js';
|
||||
import { MarineBackgroundRunner } from '../marine/worker/index.js';
|
||||
import { avmModuleLoader, controlModuleLoader } from './utilsForNode';
|
||||
import { marineLogFunction } from './utils';
|
||||
import { MarineBasedAvmRunner } from './avm';
|
||||
|
||||
import log from 'loglevel';
|
||||
import { WorkerLoaderFromFs } from '../marine/deps-loader/node.js';
|
||||
|
||||
interface EphemeralConfig {
|
||||
peers: Array<{
|
||||
peerId: PeerIdB58;
|
||||
sk: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface PeerAdapter {
|
||||
isEphemeral: boolean;
|
||||
peer: FluencePeer;
|
||||
peerId: PeerIdB58;
|
||||
onIncoming: ParticleHandler;
|
||||
connections: Set<PeerIdB58>;
|
||||
}
|
||||
|
||||
export const defaultConfig = {
|
||||
peers: [
|
||||
{
|
||||
peerId: '12D3KooWJankP2PcEDYCZDdJ26JsU8BMRfdGWyGqbtFiWyoKVtmx',
|
||||
sk: 'dWNAHhDVuFj9bEieILMu6TcCFRxBJdOPIvAWmf4sZQI=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWSBTB5sYxdwayUyTnqopBwABsnGFY3p4dTx5hABYDtJjV',
|
||||
sk: 'dOmaxAeu4Th+MJ22vRDLMFTNbiDgKNXar9fW9ofAMgQ=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWQjwf781DJ41moW5RrZXypLdnTbo6aMsoA8QLctGGX8RB',
|
||||
sk: 'TgzaLlxXuOMDNuuuTKEHUKsW0jM4AmX0gahFvkB1KgE=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWCXWTLFyY1mqKnNAhLQTsjW1zqDzCMbUs8M4a8zdz28HK',
|
||||
sk: 'hiO2Ta8g2ibMQ7iu5yj9CfN+qQCwE8oRShjr7ortKww=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWPmZpf4ng6GMS39HLagxsXbjiTPLH5CFJpFAHyN6amw6V',
|
||||
sk: 'LzJtOHTqxfrlHDW40BKiLfjai8JU4yW6/s2zrXLCcQE=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWKrx8PZxM1R9A8tp2jmrFf6c6q1ZQiWfD4QkNgh7fWSoF',
|
||||
sk: 'XMhlk/xr1FPcp7sKQhS18doXlq1x16EMhBC2NGW2LQ4=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWCbJHvnzSZEXjR1UJmtSUozuJK13iRiCYHLN1gjvm4TZZ',
|
||||
sk: 'KXPAIqxrSHr7v0ngv3qagcqivFvnQ0xd3s1/rKmi8QU=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWEvKe7WQHp42W4xhHRgTAWQjtDWyH38uJbLHAsMuTtYvD',
|
||||
sk: 'GCYMAshGnsrNtrHhuT7ayzh5uCzX99J03PmAXoOcCgw=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWSznSHN3BGrSykBXkLkFsqo9SYB73wVauVdqeuRt562cC',
|
||||
sk: 'UP+SEuznS0h259VbFquzyOJAQ4W5iIwhP+hd1PmUQQ0=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWF57jwbShfnT3c4dNfRDdGjr6SQ3B71m87UVpEpSWHFwi',
|
||||
sk: '8dl+Crm5RSh0eh+LqLKwX8/Eo4QLpvIjfD8L0wzX4A4=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWBWrzpSg9nwMLBCa2cJubUjTv63Mfy6PYg9rHGbetaV5C',
|
||||
sk: 'qolc1FcpJ+vHDon0HeXdUYnstjV1wiVx2p0mjblrfAg=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWNkLVU6juM8oyN2SVq5nBd2kp7Rf4uzJH1hET6vj6G5j6',
|
||||
sk: 'vN6QzWILTM7hSHp+iGkKxiXcqs8bzlnH3FPaRaDGSQY=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWKo1YwGL5vivPiKJMJS7wjtB6B2nJNdSXPkSABT4NKBUU',
|
||||
sk: 'YbDQ++bsor2kei7rYAsu2SbyoiOYPRzFRZWnNRUpBgQ=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWLUyBKmmNCyxaPkXoWcUFPcy5qrZsUo2E1tyM6CJmGJvC',
|
||||
sk: 'ptB9eSFMKudAtHaFgDrRK/1oIMrhBujxbMw2Pzwx/wA=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWAEZXME4KMu9FvLezsJWDbYFe2zyujyMnDT1AgcAxgcCk',
|
||||
sk: 'xtwTOKgAbDIgkuPf7RKiR7gYyZ1HY4mOgFMv3sOUcAQ=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWEhXetsFVAD9h2dRz9XgFpfidho1TCZVhFrczX8h8qgzY',
|
||||
sk: '1I2MGuiKG1F4FDMiRihVOcOP2mxzOLWJ99MeexK27A4=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWDBfVNdMyV3hPEF4WLBmx9DwD2t2SYuqZ2mztYmDzZWM1',
|
||||
sk: 'eqJ4Bp7iN4aBXgPH0ezwSg+nVsatkYtfrXv9obI0YQ0=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWSyY7wiSiR4vbXa1WtZawi3ackMTqcQhEPrvqtagoWPny',
|
||||
sk: 'UVM3SBJhPYIY/gafpnd9/q/Fn9V4BE9zkgrvF1T7Pgc=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWFZmBMGG9PxTs9s6ASzkLGKJWMyPheA5ruaYc2FDkDTmv',
|
||||
sk: '8RbZfEVpQhPVuhv64uqxENDuSoyJrslQoSQJznxsTQ0=',
|
||||
},
|
||||
{
|
||||
peerId: '12D3KooWBbhUaqqur6KHPunnKxXjY1daCtqJdy4wRji89LmAkVB4',
|
||||
sk: 'RbgKmG6soWW9uOi7yRedm+0Qck3f3rw6MSnDP7AcBQs=',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Ephemeral network implementation.
|
||||
* Ephemeral network is a virtual network which runs locally and focuses on p2p interaction by removing connectivity layer out of the equation.
|
||||
*/
|
||||
export class EphemeralNetwork {
|
||||
private _peers: Map<PeerIdB58, PeerAdapter> = new Map();
|
||||
|
||||
constructor(public readonly config: EphemeralConfig) {}
|
||||
|
||||
/**
|
||||
* Starts the Ephemeral network up
|
||||
*/
|
||||
async up(): Promise<void> {
|
||||
log.debug('Starting ephemeral network up...');
|
||||
const allPeerIds = this.config.peers.map((x) => x.peerId);
|
||||
// shared worker for all the peers
|
||||
const workerLoader = new WorkerLoaderFromFs('../../marine/worker-script');
|
||||
|
||||
const promises = this.config.peers.map(async (x) => {
|
||||
const logLevel = undefined;
|
||||
const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader, marineLogFunction);
|
||||
const avm = new MarineBasedAvmRunner(marine, avmModuleLoader, logLevel);
|
||||
const peer = new FluencePeer(marine, avm);
|
||||
const sendParticle = async (nextPeerIds: string[], particle: string): Promise<void> => {
|
||||
this._send(peer.getStatus().peerId!, nextPeerIds, particle);
|
||||
};
|
||||
const kp = await fromBase64Sk(x.sk);
|
||||
if (kp.getPeerId() !== x.peerId) {
|
||||
throw new Error(`Invalid config: peer id ${x.peerId} does not match the secret key ${x.sk}`);
|
||||
}
|
||||
await peer.init({}, kp);
|
||||
|
||||
let handler: ParticleHandler | null = null;
|
||||
const connectionCtor = class extends FluenceConnection {
|
||||
relayPeerId = null;
|
||||
|
||||
async connect(onIncomingParticle: ParticleHandler): Promise<void> {
|
||||
handler = onIncomingParticle;
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
handler = null;
|
||||
}
|
||||
|
||||
sendParticle = sendParticle;
|
||||
};
|
||||
|
||||
await peer.connect(new connectionCtor());
|
||||
|
||||
const peerId = peer.getStatus().peerId!;
|
||||
const ephPeer: PeerAdapter = {
|
||||
isEphemeral: true,
|
||||
connections: new Set(allPeerIds.filter((x) => x !== peerId)),
|
||||
peer: peer,
|
||||
peerId: peerId,
|
||||
onIncoming: handler!,
|
||||
};
|
||||
return [peerId, ephPeer] as const;
|
||||
});
|
||||
const values = await Promise.all(promises);
|
||||
this._peers = new Map(values);
|
||||
log.debug('Ephemeral network started...');
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts the ephemeral network down. Will disconnect all connected peers.
|
||||
*/
|
||||
async down(): Promise<void> {
|
||||
log.debug('Shutting down ephemeral network...');
|
||||
const peers = Array.from(this._peers.entries());
|
||||
const promises = peers.map(([k, p]) => {
|
||||
return p.isEphemeral ? p.peer.stop() : p.peer.disconnect();
|
||||
});
|
||||
await Promise.all(promises);
|
||||
this._peers.clear();
|
||||
log.debug('Ephemeral network shut down');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the FluenceConnection which can be used to connect to the ephemeral networks via the specified relay peer.
|
||||
*/
|
||||
getRelayConnection(relay: PeerIdB58, peer: FluencePeer): FluenceConnection {
|
||||
const me = this;
|
||||
const relayPeer = this._peers.get(relay);
|
||||
if (relayPeer === undefined) {
|
||||
throw new Error(`Relay with peer Id: ${relay} has not been found in ephemeral network`);
|
||||
}
|
||||
const connectionCtor = class extends FluenceConnection {
|
||||
relayPeerId = relay;
|
||||
|
||||
async connect(onIncomingParticle: ParticleHandler): Promise<void> {
|
||||
const peerId = peer.getStatus().peerId!;
|
||||
me._peers.set(peerId, {
|
||||
isEphemeral: false,
|
||||
peer: peer,
|
||||
onIncoming: onIncomingParticle,
|
||||
peerId: peerId,
|
||||
connections: new Set([relay]),
|
||||
});
|
||||
relayPeer.connections.add(peerId);
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
const peerId = peer.getStatus().peerId!;
|
||||
relayPeer.connections.delete(peerId);
|
||||
me._peers.delete(peerId);
|
||||
}
|
||||
async sendParticle(nextPeerIds: string[], particle: string): Promise<void> {
|
||||
const peerId = peer.getStatus().peerId!;
|
||||
me._send(peerId, nextPeerIds, particle);
|
||||
}
|
||||
};
|
||||
return new connectionCtor();
|
||||
}
|
||||
|
||||
private async _send(from: PeerIdB58, to: PeerIdB58[], particle: string) {
|
||||
log.info(`Sending particle from ${from}, to ${JSON.stringify(to)}`);
|
||||
const peer = this._peers.get(from);
|
||||
if (peer === undefined) {
|
||||
log.error(`Peer ${from} cannot be found in ephemeral network`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let dest of to) {
|
||||
if (!peer.connections.has(dest)) {
|
||||
log.error(`Peer ${from} has no connection with ${dest}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const destPeer = this._peers.get(dest);
|
||||
if (destPeer === undefined) {
|
||||
log.error(`peer ${destPeer} cannot be found in ephemeral network`);
|
||||
continue;
|
||||
}
|
||||
|
||||
destPeer.onIncoming(particle);
|
||||
}
|
||||
}
|
||||
}
|
188
packages/core/js-peer/src/js-peer/utils.ts
Normal file
188
packages/core/js-peer/src/js-peer/utils.ts
Normal file
@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright 2021 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import log from 'loglevel';
|
||||
|
||||
import { Buffer } from 'buffer';
|
||||
import { CallServiceData, CallServiceResult, CallServiceResultType, ResultCodes } from '../interfaces/commonTypes.js';
|
||||
import { FluencePeer } from './FluencePeer.js';
|
||||
import { ParticleExecutionStage } from './Particle.js';
|
||||
import { LogFunction } from '@fluencelabs/marine-js/dist/types';
|
||||
|
||||
export const MakeServiceCall =
|
||||
(fn: (args: any[]) => CallServiceResultType) =>
|
||||
(req: CallServiceData): CallServiceResult => ({
|
||||
retCode: ResultCodes.success,
|
||||
result: fn(req.args),
|
||||
});
|
||||
|
||||
export const handleTimeout = (fn: () => void) => (stage: ParticleExecutionStage) => {
|
||||
if (stage.stage === 'expired') {
|
||||
fn();
|
||||
}
|
||||
};
|
||||
export const doNothing = (..._args: Array<unknown>) => undefined;
|
||||
|
||||
/**
|
||||
* Checks the network connection by sending a ping-like request to relay node
|
||||
* @param { FluenceClient } peer - The Fluence Client instance.
|
||||
*/
|
||||
export const checkConnection = async (peer: FluencePeer, ttl?: number): Promise<boolean> => {
|
||||
if (!peer.getStatus().isConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const msg = Math.random().toString(36).substring(7);
|
||||
|
||||
const promise = new Promise<string>((resolve, reject) => {
|
||||
const script = `
|
||||
(xor
|
||||
(seq
|
||||
(call %init_peer_id% ("load" "relay") [] init_relay)
|
||||
(seq
|
||||
(call %init_peer_id% ("load" "msg") [] msg)
|
||||
(seq
|
||||
(call init_relay ("op" "identity") [msg] result)
|
||||
(call %init_peer_id% ("callback" "callback") [result])
|
||||
)
|
||||
)
|
||||
)
|
||||
(seq
|
||||
(call init_relay ("op" "identity") [])
|
||||
(call %init_peer_id% ("callback" "error") [%last_error%])
|
||||
)
|
||||
)`;
|
||||
const particle = peer.internals.createNewParticle(script, ttl);
|
||||
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
||||
peer.internals.regHandler.forParticle(
|
||||
particle.id,
|
||||
'load',
|
||||
'relay',
|
||||
MakeServiceCall(() => {
|
||||
return peer.getStatus().relayPeerId;
|
||||
}),
|
||||
);
|
||||
|
||||
peer.internals.regHandler.forParticle(
|
||||
particle.id,
|
||||
'load',
|
||||
'msg',
|
||||
MakeServiceCall(() => {
|
||||
return msg;
|
||||
}),
|
||||
);
|
||||
|
||||
peer.internals.regHandler.forParticle(
|
||||
particle.id,
|
||||
'callback',
|
||||
'callback',
|
||||
MakeServiceCall((args) => {
|
||||
const [val] = args;
|
||||
setTimeout(() => {
|
||||
resolve(val);
|
||||
}, 0);
|
||||
return {};
|
||||
}),
|
||||
);
|
||||
|
||||
peer.internals.regHandler.forParticle(
|
||||
particle.id,
|
||||
'callback',
|
||||
'error',
|
||||
MakeServiceCall((args) => {
|
||||
const [error] = args;
|
||||
setTimeout(() => {
|
||||
reject(error);
|
||||
}, 0);
|
||||
return {};
|
||||
}),
|
||||
);
|
||||
|
||||
peer.internals.initiateParticle(
|
||||
particle,
|
||||
handleTimeout(() => {
|
||||
reject('particle timed out');
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
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 function dataToString(data: Uint8Array) {
|
||||
const text = new TextDecoder().decode(Buffer.from(data));
|
||||
// try to treat data as json and pretty-print it
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(text), null, 4);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
export function jsonify(obj: unknown) {
|
||||
return JSON.stringify(obj, null, 4);
|
||||
}
|
||||
|
||||
export const isString = (x: unknown): x is string => {
|
||||
return x !== null && typeof x === 'string';
|
||||
};
|
||||
|
||||
export class ServiceError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
|
||||
Object.setPrototypeOf(this, ServiceError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export const marineLogFunction: LogFunction = (message) => {
|
||||
const str = `[marine service "${message.service}"]: ${message.message}`;
|
||||
|
||||
const nodeProcess = (globalThis as any).process ? (globalThis as any).process : undefined;
|
||||
if (nodeProcess && nodeProcess.stderr) {
|
||||
nodeProcess.stderr.write(str);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message.level) {
|
||||
case 'warn':
|
||||
console.warn(str);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error(str);
|
||||
break;
|
||||
|
||||
case 'debug':
|
||||
case 'trace':
|
||||
case 'info':
|
||||
console.log(str);
|
||||
break;
|
||||
}
|
||||
};
|
5
packages/core/js-peer/src/js-peer/utilsForNode.ts
Normal file
5
packages/core/js-peer/src/js-peer/utilsForNode.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { WorkerLoaderFromFs, WasmLoaderFromFs, WasmLoaderFromNpm } from '../marine/deps-loader/node.js';
|
||||
|
||||
// TODO!: after moving to ESM loaders stopped working. Should be fixed in scope of DXJ-194
|
||||
export const controlModuleLoader = new WasmLoaderFromNpm('@fluencelabs/marine-js', 'marine-js.wasm');
|
||||
export const avmModuleLoader = new WasmLoaderFromNpm('@fluencelabs/avm', 'avm.wasm');
|
94
packages/core/js-peer/src/keypair/__test__/KeyPair.spec.ts
Normal file
94
packages/core/js-peer/src/keypair/__test__/KeyPair.spec.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { toUint8Array } from 'js-base64';
|
||||
import * as bs58 from 'bs58';
|
||||
import { KeyPair } from '../index.js';
|
||||
|
||||
// @ts-ignore
|
||||
const { decode } = bs58.default;
|
||||
|
||||
const key = '+cmeYlZKj+MfSa9dpHV+BmLPm6wq4inGlsPlQ1GvtPk=';
|
||||
const keyBytes = toUint8Array(key);
|
||||
|
||||
const testData = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 9, 10]);
|
||||
|
||||
const testDataSig = Uint8Array.from([
|
||||
224, 104, 245, 206, 140, 248, 27, 72, 68, 133, 111, 10, 164, 197, 242, 132, 107, 77, 224, 67, 99, 106, 76, 29, 144,
|
||||
121, 122, 169, 36, 173, 58, 80, 170, 102, 137, 253, 157, 247, 168, 87, 162, 223, 188, 214, 203, 220, 52, 246, 29,
|
||||
86, 77, 71, 224, 248, 16, 213, 254, 75, 78, 239, 243, 222, 241, 15,
|
||||
]);
|
||||
|
||||
// signature produced by KeyPair created from some random KeyPair
|
||||
|
||||
describe('KeyPair tests', () => {
|
||||
it('generate keypair from seed', async function () {
|
||||
// arrange
|
||||
const random = await KeyPair.randomEd25519();
|
||||
const privateKey = random.toEd25519PrivateKey();
|
||||
|
||||
// act
|
||||
const keyPair = await KeyPair.fromEd25519SK(privateKey);
|
||||
const privateKey2 = keyPair.toEd25519PrivateKey();
|
||||
|
||||
// assert
|
||||
expect(privateKey).toStrictEqual(privateKey2);
|
||||
});
|
||||
|
||||
it('create keypair from ed25519 private key', async function () {
|
||||
// arrange
|
||||
const rustSK = 'jDaxLJzYtzgwTMrELJCAqavtmx85ktQNfB2rLcK7MhH';
|
||||
const sk = decode(rustSK);
|
||||
|
||||
// act
|
||||
const keyPair = await KeyPair.fromEd25519SK(sk);
|
||||
|
||||
// assert
|
||||
const expectedPeerId = '12D3KooWH1W3VznVZ87JH4FwABK4mkntcspTVWJDta6c2xg9Pzbp';
|
||||
expect(keyPair.getPeerId()).toStrictEqual(expectedPeerId);
|
||||
});
|
||||
|
||||
it('create keypair from a seed phrase', async function () {
|
||||
// arrange
|
||||
const seedArray = new Uint8Array(32).fill(1);
|
||||
|
||||
// act
|
||||
const keyPair = await KeyPair.fromEd25519SK(seedArray);
|
||||
|
||||
// assert
|
||||
const expectedPeerId = '12D3KooWK99VoVxNE7XzyBwXEzW7xhK7Gpv85r9F3V3fyKSUKPH5';
|
||||
expect(keyPair.getPeerId()).toStrictEqual(expectedPeerId);
|
||||
});
|
||||
|
||||
it('sign', async function () {
|
||||
// arrange
|
||||
const keyPair = await KeyPair.fromEd25519SK(keyBytes);
|
||||
|
||||
// act
|
||||
const res = await keyPair.signBytes(testData);
|
||||
|
||||
// assert
|
||||
expect(res).toStrictEqual(testDataSig);
|
||||
});
|
||||
|
||||
it('verify', async function () {
|
||||
// arrange
|
||||
const keyPair = await KeyPair.fromEd25519SK(keyBytes);
|
||||
|
||||
// act
|
||||
const res = await keyPair.verify(testData, testDataSig);
|
||||
|
||||
// assert
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
|
||||
it('sign-verify', async function () {
|
||||
// arrange
|
||||
const keyPair = await KeyPair.fromEd25519SK(keyBytes);
|
||||
|
||||
// act
|
||||
const data = new Uint8Array(32).fill(1);
|
||||
const sig = await keyPair.signBytes(data);
|
||||
const res = await keyPair.verify(data, sig);
|
||||
|
||||
// assert
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
});
|
95
packages/core/js-peer/src/keypair/index.ts
Normal file
95
packages/core/js-peer/src/keypair/index.ts
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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 type { PeerId } from '@libp2p/interface-peer-id';
|
||||
import { generateKeyPairFromSeed, generateKeyPair } from '@libp2p/crypto/keys';
|
||||
import { createFromPrivKey } from '@libp2p/peer-id-factory';
|
||||
import type { PrivateKey } from '@libp2p/interface-keys';
|
||||
import { toUint8Array } from 'js-base64';
|
||||
import * as bs58 from 'bs58';
|
||||
import { KeyPairOptions } from '@fluencelabs/interfaces';
|
||||
|
||||
// @ts-ignore
|
||||
const { decode } = bs58.default;
|
||||
|
||||
export class KeyPair {
|
||||
/**
|
||||
* Key pair in libp2p format. Used for backward compatibility with the current FluencePeer implementation
|
||||
*/
|
||||
getLibp2pPeerId() {
|
||||
return this.libp2pPeerId;
|
||||
}
|
||||
|
||||
constructor(private key: PrivateKey, private libp2pPeerId: PeerId) {}
|
||||
|
||||
/**
|
||||
* Generates new KeyPair from ed25519 private key represented as a 32 byte array
|
||||
* @param seed - Any sequence of 32 bytes
|
||||
* @returns - Promise with the created KeyPair
|
||||
*/
|
||||
static async fromEd25519SK(seed: Uint8Array): Promise<KeyPair> {
|
||||
const key = await generateKeyPairFromSeed('Ed25519', seed, 256);
|
||||
const lib2p2Pid = await createFromPrivKey(key);
|
||||
return new KeyPair(key, lib2p2Pid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates new KeyPair with a random secret key
|
||||
* @returns - Promise with the created KeyPair
|
||||
*/
|
||||
static async randomEd25519(): Promise<KeyPair> {
|
||||
const key = await generateKeyPair('Ed25519');
|
||||
const lib2p2Pid = await createFromPrivKey(key);
|
||||
return new KeyPair(key, lib2p2Pid);
|
||||
}
|
||||
|
||||
getPeerId(): string {
|
||||
return this.libp2pPeerId.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns 32 byte private key
|
||||
*/
|
||||
toEd25519PrivateKey(): Uint8Array {
|
||||
return this.key.marshal().subarray(0, 32);
|
||||
}
|
||||
|
||||
signBytes(data: Uint8Array): Promise<Uint8Array> {
|
||||
return this.key.sign(data);
|
||||
}
|
||||
|
||||
verify(data: Uint8Array, signature: Uint8Array): Promise<boolean> {
|
||||
return this.key.public.verify(data, signature);
|
||||
}
|
||||
}
|
||||
|
||||
export const fromBase64Sk = (sk: string): Promise<KeyPair> => {
|
||||
const skArr = toUint8Array(sk);
|
||||
return KeyPair.fromEd25519SK(skArr);
|
||||
};
|
||||
|
||||
export const fromBase58Sk = (sk: string): Promise<KeyPair> => {
|
||||
const skArr = decode(sk);
|
||||
return KeyPair.fromEd25519SK(skArr);
|
||||
};
|
||||
|
||||
export const fromOpts = (opts: KeyPairOptions): Promise<KeyPair> => {
|
||||
if (opts.source === 'random') {
|
||||
return KeyPair.randomEd25519();
|
||||
}
|
||||
|
||||
return KeyPair.fromEd25519SK(opts.source);
|
||||
};
|
23
packages/core/js-peer/src/marine/deps-loader/common.ts
Normal file
23
packages/core/js-peer/src/marine/deps-loader/common.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { BlobWorker } from 'threads';
|
||||
import { fromBase64, toUint8Array } from 'js-base64';
|
||||
import type { WorkerImplementation } from 'threads/dist/types/master';
|
||||
import { LazyLoader } from '../../interfaces/index.js';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
export class InlinedWorkerLoader extends LazyLoader<WorkerImplementation> {
|
||||
constructor(b64script: string) {
|
||||
super(() => {
|
||||
const script = fromBase64(b64script);
|
||||
return BlobWorker.fromText(script);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class InlinedWasmLoader extends LazyLoader<Buffer> {
|
||||
constructor(b64wasm: string) {
|
||||
super(() => {
|
||||
const wasm = toUint8Array(b64wasm);
|
||||
return Buffer.from(wasm);
|
||||
});
|
||||
}
|
||||
}
|
68
packages/core/js-peer/src/marine/deps-loader/node.ts
Normal file
68
packages/core/js-peer/src/marine/deps-loader/node.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { createRequire } from 'module';
|
||||
import { LazyLoader } from '../../interfaces/index.js';
|
||||
|
||||
import type { WorkerImplementation } from 'threads/dist/types/master';
|
||||
import { Worker } from 'threads';
|
||||
import { Buffer } from 'buffer';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const bufferToSharedArrayBuffer = (buffer: Buffer): SharedArrayBuffer => {
|
||||
const sab = new SharedArrayBuffer(buffer.length);
|
||||
const tmp = new Uint8Array(sab);
|
||||
tmp.set(buffer, 0);
|
||||
return sab;
|
||||
};
|
||||
|
||||
/**
|
||||
* Load wasm file from npm package. Only works in nodejs environment.
|
||||
* The function returns SharedArrayBuffer compatible with FluenceAppService methods.
|
||||
* @param source - object specifying the source of the file. Consist two fields: package name and file path.
|
||||
* @returns SharedArrayBuffer with the wasm file
|
||||
*/
|
||||
export const loadWasmFromNpmPackage = async (source: { package: string; file: string }): Promise<SharedArrayBuffer> => {
|
||||
const packagePath = require.resolve(source.package);
|
||||
const filePath = path.join(path.dirname(packagePath), source.file);
|
||||
return loadWasmFromFileSystem(filePath);
|
||||
};
|
||||
|
||||
/**
|
||||
* Load wasm file from the file system. Only works in nodejs environment.
|
||||
* The functions returns SharedArrayBuffer compatible with FluenceAppService methods.
|
||||
* @param filePath - path to the wasm file
|
||||
* @returns SharedArrayBuffer with the wasm fileWorker
|
||||
*/
|
||||
export const loadWasmFromFileSystem = async (filePath: string): Promise<SharedArrayBuffer> => {
|
||||
const buffer = await fs.promises.readFile(filePath);
|
||||
return bufferToSharedArrayBuffer(buffer);
|
||||
};
|
||||
|
||||
export class WasmLoaderFromFs extends LazyLoader<SharedArrayBuffer> {
|
||||
constructor(filePath: string) {
|
||||
super(() => loadWasmFromFileSystem(filePath));
|
||||
}
|
||||
}
|
||||
|
||||
export class WasmLoaderFromNpm extends LazyLoader<SharedArrayBuffer> {
|
||||
constructor(pkg: string, file: string) {
|
||||
super(() => loadWasmFromNpmPackage({ package: pkg, file: file }));
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkerLoaderFromFs extends LazyLoader<WorkerImplementation> {
|
||||
constructor(scriptPath: string) {
|
||||
super(() => new Worker(scriptPath));
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkerLoaderFromNpm extends LazyLoader<WorkerImplementation> {
|
||||
constructor(pkg: string, file: string) {
|
||||
super(() => {
|
||||
const packagePath = require.resolve(pkg);
|
||||
const scriptPath = path.join(path.dirname(packagePath), file);
|
||||
return new Worker(scriptPath);
|
||||
});
|
||||
}
|
||||
}
|
40
packages/core/js-peer/src/marine/deps-loader/web.ts
Normal file
40
packages/core/js-peer/src/marine/deps-loader/web.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Buffer } from 'buffer';
|
||||
import { LazyLoader } from '../../interfaces/index.js';
|
||||
|
||||
const bufferToSharedArrayBuffer = (buffer: Buffer): SharedArrayBuffer => {
|
||||
const sab = new SharedArrayBuffer(buffer.length);
|
||||
const tmp = new Uint8Array(sab);
|
||||
tmp.set(buffer, 0);
|
||||
return sab;
|
||||
};
|
||||
|
||||
/**
|
||||
* Load wasm file from the server. Only works in browsers.
|
||||
* The function will try load file into SharedArrayBuffer if the site is cross-origin isolated.
|
||||
* Otherwise the return value fallbacks to Buffer which is less performant but is still compatible with FluenceAppService methods.
|
||||
* We strongly recommend to set-up cross-origin headers. For more details see: See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements
|
||||
* Filename is relative to current origin.
|
||||
* @param filePath - path to the wasm file relative to current origin
|
||||
* @returns Either SharedArrayBuffer or Buffer with the wasm file
|
||||
*/
|
||||
export const loadWasmFromServer = async (filePath: string): Promise<SharedArrayBuffer | Buffer> => {
|
||||
const fullUrl = window.location.origin + '/' + filePath;
|
||||
const res = await fetch(fullUrl);
|
||||
const ab = await res.arrayBuffer();
|
||||
new Uint8Array(ab);
|
||||
const buffer = Buffer.from(ab);
|
||||
|
||||
// only convert to shared buffers if necessary CORS headers have been set:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements
|
||||
if (crossOriginIsolated) {
|
||||
return bufferToSharedArrayBuffer(buffer);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
};
|
||||
|
||||
export class WebLoaderFromUrl extends LazyLoader<SharedArrayBuffer | Buffer> {
|
||||
constructor(filePath: string) {
|
||||
super(() => loadWasmFromServer(filePath));
|
||||
}
|
||||
}
|
83
packages/core/js-peer/src/marine/worker-script/index.ts
Normal file
83
packages/core/js-peer/src/marine/worker-script/index.ts
Normal file
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2022 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 { MarineService } from '@fluencelabs/marine-js/dist/MarineService';
|
||||
import type { Env, MarineServiceConfig } from '@fluencelabs/marine-js/dist/config';
|
||||
import type { JSONArray, JSONObject, LogMessage } from '@fluencelabs/marine-js/dist/types';
|
||||
import { Observable, Subject } from 'threads/observable';
|
||||
import { expose } from 'threads/worker';
|
||||
|
||||
let marineServices = new Map<string, MarineService>();
|
||||
let controlModule: WebAssembly.Module | undefined;
|
||||
|
||||
const onLogMessage = new Subject<LogMessage>();
|
||||
|
||||
const asArray = (buf: SharedArrayBuffer | Buffer) => {
|
||||
return new Uint8Array(buf);
|
||||
};
|
||||
|
||||
const toExpose = {
|
||||
init: async (controlModuleWasm: SharedArrayBuffer | Buffer): Promise<void> => {
|
||||
controlModule = await WebAssembly.compile(asArray(controlModuleWasm));
|
||||
},
|
||||
|
||||
createService: async (
|
||||
wasm: SharedArrayBuffer | Buffer,
|
||||
serviceId: string,
|
||||
marineConfig?: MarineServiceConfig,
|
||||
envs?: Env,
|
||||
): Promise<void> => {
|
||||
if (!controlModule) {
|
||||
throw new Error('MarineJS is not initialized. To initialize call `init` function');
|
||||
}
|
||||
|
||||
const service = await WebAssembly.compile(asArray(wasm));
|
||||
const srv = new MarineService(
|
||||
controlModule,
|
||||
service,
|
||||
serviceId,
|
||||
onLogMessage.next.bind(onLogMessage),
|
||||
marineConfig,
|
||||
envs,
|
||||
);
|
||||
await srv.init();
|
||||
marineServices.set(serviceId, srv);
|
||||
},
|
||||
|
||||
terminate: () => {
|
||||
marineServices.forEach((val, key) => {
|
||||
val.terminate();
|
||||
});
|
||||
onLogMessage.complete();
|
||||
},
|
||||
|
||||
callService: (serviceId: string, functionName: string, args: JSONArray | JSONObject, callParams: any): unknown => {
|
||||
const srv = marineServices.get(serviceId);
|
||||
if (!srv) {
|
||||
throw new Error(`service with id=${serviceId} not found`);
|
||||
}
|
||||
|
||||
return srv.call(functionName, args, callParams);
|
||||
},
|
||||
|
||||
onLogMessage(): Observable<LogMessage> {
|
||||
return Observable.from(onLogMessage);
|
||||
},
|
||||
};
|
||||
|
||||
export type MarineBackgroundInterface = typeof toExpose;
|
||||
|
||||
expose(toExpose);
|
@ -0,0 +1,10 @@
|
||||
import { LazyLoader } from '../../interfaces/index.js';
|
||||
|
||||
import type { WorkerImplementation } from 'threads/dist/types/master';
|
||||
import { Worker } from 'threads';
|
||||
|
||||
export class WorkerLoader extends LazyLoader<WorkerImplementation> {
|
||||
constructor() {
|
||||
super(() => new Worker('./'));
|
||||
}
|
||||
}
|
77
packages/core/js-peer/src/marine/worker/index.ts
Normal file
77
packages/core/js-peer/src/marine/worker/index.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2022 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { JSONArray, JSONObject, LogLevel } from '@fluencelabs/marine-js/dist/types';
|
||||
import { LogFunction, logLevelToEnv } from '@fluencelabs/marine-js/dist/types';
|
||||
import type { IMarine, IWorkerLoader, IWasmLoader } from '../../interfaces/index.js';
|
||||
import type { MarineBackgroundInterface } from '../worker-script/index.js';
|
||||
import { spawn, Thread } from 'threads';
|
||||
import type { ModuleThread } from 'threads';
|
||||
|
||||
export class MarineBackgroundRunner implements IMarine {
|
||||
private workerThread?: ModuleThread<MarineBackgroundInterface>;
|
||||
|
||||
constructor(
|
||||
private workerLoader: IWorkerLoader,
|
||||
private controlModuleLoader: IWasmLoader,
|
||||
private logFunction: LogFunction,
|
||||
) {}
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this.workerThread) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.workerLoader.start();
|
||||
await this.controlModuleLoader.start();
|
||||
const worker = this.workerLoader.getValue();
|
||||
const wasm = this.controlModuleLoader.getValue();
|
||||
this.workerThread = await spawn<MarineBackgroundInterface>(worker, { timeout: 99999999 });
|
||||
this.workerThread.onLogMessage().subscribe(this.logFunction);
|
||||
await this.workerThread.init(wasm);
|
||||
}
|
||||
|
||||
createService(serviceModule: SharedArrayBuffer | Buffer, serviceId: string, logLevel?: LogLevel): Promise<void> {
|
||||
if (!this.workerThread) {
|
||||
throw 'Worker is not initialized';
|
||||
}
|
||||
|
||||
const env = logLevel ? logLevelToEnv(logLevel) : {};
|
||||
return this.workerThread.createService(serviceModule, serviceId, undefined, env);
|
||||
}
|
||||
|
||||
callService(
|
||||
serviceId: string,
|
||||
functionName: string,
|
||||
args: JSONArray | JSONObject,
|
||||
callParams: any,
|
||||
): Promise<unknown> {
|
||||
if (!this.workerThread) {
|
||||
throw 'Worker is not initialized';
|
||||
}
|
||||
|
||||
return this.workerThread.callService(serviceId, functionName, args, callParams);
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (!this.workerThread) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.workerThread.terminate();
|
||||
await Thread.terminate(this.workerThread);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user