mirror of
https://github.com/fluencelabs/fluence-js.git
synced 2025-06-12 23:51:21 +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:
48
packages/core/interfaces/package.json
Normal file
48
packages/core/interfaces/package.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@fluencelabs/interfaces",
|
||||
"type": "module",
|
||||
"version": "0.5.0",
|
||||
"description": "Interfaces",
|
||||
"main": "./dist/index.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./fluenceClient": {
|
||||
"import": "./dist/fluenceClient.js",
|
||||
"types": "./dist/fluenceClient.d.ts"
|
||||
},
|
||||
"./compilerSupport": {
|
||||
"import": "./dist/compilerSupport.js",
|
||||
"types": "./dist/compilerSupport.d.ts"
|
||||
}
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"fluenceClient.d.ts": [
|
||||
"./dist/fluenceClient.d.ts"
|
||||
],
|
||||
"compilerSupport.d.ts": [
|
||||
"./dist/compilerSupport.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"pnpm": ">=3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
},
|
||||
"repository": "https://github.com/fluencelabs/fluence-js",
|
||||
"author": "Fluence Labs",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@multiformats/multiaddr": "11.3.0",
|
||||
"@fluencelabs/avm": "0.35.3",
|
||||
"@fluencelabs/marine-js": "0.3.44"
|
||||
}
|
||||
}
|
258
packages/core/interfaces/src/compilerSupport.ts
Normal file
258
packages/core/interfaces/src/compilerSupport.ts
Normal file
@ -0,0 +1,258 @@
|
||||
type SomeNonArrowTypes = ScalarType | OptionType | ArrayType | StructType | TopType | BottomType;
|
||||
|
||||
export type NonArrowType = SomeNonArrowTypes | ProductType<SomeNonArrowTypes>;
|
||||
|
||||
export type TopType = {
|
||||
/**
|
||||
* Type descriptor. Used for pattern-matching
|
||||
*/
|
||||
tag: 'topType';
|
||||
};
|
||||
|
||||
export type BottomType = {
|
||||
/**
|
||||
* Type descriptor. Used for pattern-matching
|
||||
*/
|
||||
tag: 'bottomType';
|
||||
};
|
||||
|
||||
export type OptionType = {
|
||||
/**
|
||||
* Type descriptor. Used for pattern-matching
|
||||
*/
|
||||
tag: 'option';
|
||||
|
||||
/**
|
||||
* Underlying type of the option
|
||||
*/
|
||||
type: NonArrowType;
|
||||
};
|
||||
|
||||
export type NilType = {
|
||||
/**
|
||||
* Type descriptor. Used for pattern-matching
|
||||
*/
|
||||
tag: 'nil';
|
||||
};
|
||||
|
||||
export type ArrayType = {
|
||||
/**
|
||||
* Type descriptor. Used for pattern-matching
|
||||
*/
|
||||
tag: 'array';
|
||||
|
||||
/**
|
||||
* Type of array elements
|
||||
*/
|
||||
type: NonArrowType;
|
||||
};
|
||||
|
||||
/**
|
||||
* All possible scalar type names
|
||||
*/
|
||||
export type ScalarNames =
|
||||
| 'u8'
|
||||
| 'u16'
|
||||
| 'u32'
|
||||
| 'u64'
|
||||
| 'i8'
|
||||
| 'i16'
|
||||
| 'i32'
|
||||
| 'i64'
|
||||
| 'f32'
|
||||
| 'f64'
|
||||
| 'bool'
|
||||
| 'string';
|
||||
|
||||
export type ScalarType = {
|
||||
/**
|
||||
* Type descriptor. Used for pattern-matching
|
||||
*/
|
||||
tag: 'scalar';
|
||||
|
||||
/**
|
||||
* Name of the scalar type
|
||||
*/
|
||||
name: ScalarNames;
|
||||
};
|
||||
|
||||
export type StructType = {
|
||||
/**
|
||||
* Type descriptor. Used for pattern-matching
|
||||
*/
|
||||
tag: 'struct';
|
||||
|
||||
/**
|
||||
* Struct name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Struct fields
|
||||
*/
|
||||
fields: { [key: string]: NonArrowType };
|
||||
};
|
||||
|
||||
export type LabeledProductType<T> =
|
||||
| {
|
||||
/**
|
||||
* Type descriptor. Used for pattern-matching
|
||||
*/
|
||||
tag: 'labeledProduct';
|
||||
|
||||
/**
|
||||
* Labelled product fields
|
||||
*/
|
||||
fields: { [key: string]: T };
|
||||
}
|
||||
| NilType;
|
||||
|
||||
export type UnlabeledProductType<T> =
|
||||
| {
|
||||
/**
|
||||
* Type descriptor. Used for pattern-matching
|
||||
*/
|
||||
tag: 'unlabeledProduct';
|
||||
|
||||
/**
|
||||
* Items in unlabelled product
|
||||
*/
|
||||
items: Array<T>;
|
||||
}
|
||||
| NilType;
|
||||
|
||||
export type ProductType<T> = UnlabeledProductType<T> | LabeledProductType<T>;
|
||||
|
||||
/**
|
||||
* ArrowType is a profunctor pointing its domain to codomain.
|
||||
* Profunctor means variance: Arrow is contravariant on domain, and variant on codomain.
|
||||
*/
|
||||
export type ArrowType<T> = {
|
||||
/**
|
||||
* Type descriptor. Used for pattern-matching
|
||||
*/
|
||||
tag: 'arrow';
|
||||
|
||||
/**
|
||||
* Where this Arrow is defined
|
||||
*/
|
||||
domain: ProductType<T>;
|
||||
|
||||
/**
|
||||
* Where this Arrow points to
|
||||
*/
|
||||
codomain: UnlabeledProductType<NonArrowType> | NilType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Arrow which domain contains only non-arrow types
|
||||
*/
|
||||
export type ArrowWithoutCallbacks = ArrowType<NonArrowType>;
|
||||
|
||||
/**
|
||||
* Arrow which domain does can contain both non-arrow types and arrows (which themselves cannot contain arrows)
|
||||
*/
|
||||
export type ArrowWithCallbacks = ArrowType<NonArrowType | ArrowWithoutCallbacks>;
|
||||
|
||||
export interface FunctionCallConstants {
|
||||
/**
|
||||
* The name of the relay variable
|
||||
*/
|
||||
relay: string;
|
||||
|
||||
/**
|
||||
* The name of the serviceId used load variables at the beginning of the script
|
||||
*/
|
||||
getDataSrv: string;
|
||||
|
||||
/**
|
||||
* The name of serviceId is used to execute callbacks for the current particle
|
||||
*/
|
||||
callbackSrv: string;
|
||||
|
||||
/**
|
||||
* The name of the serviceId which is called to propagate return value to the generated function caller
|
||||
*/
|
||||
responseSrv: string;
|
||||
|
||||
/**
|
||||
* The name of the functionName which is called to propagate return value to the generated function caller
|
||||
*/
|
||||
responseFnName: string;
|
||||
|
||||
/**
|
||||
* The name of the serviceId which is called to report errors to the generated function caller
|
||||
*/
|
||||
errorHandlingSrv: string;
|
||||
|
||||
/**
|
||||
* The name of the functionName which is called to report errors to the generated function caller
|
||||
*/
|
||||
errorFnName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Definition of function (`func` instruction) generated by the Aqua compiler
|
||||
*/
|
||||
export interface FunctionCallDef {
|
||||
/**
|
||||
* The name of the function in Aqua language
|
||||
*/
|
||||
functionName: string;
|
||||
|
||||
/**
|
||||
* Underlying arrow which represents function in aqua
|
||||
*/
|
||||
arrow: ArrowWithCallbacks;
|
||||
|
||||
/**
|
||||
* Names of the different entities used in generated air script
|
||||
*/
|
||||
names: FunctionCallConstants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Definition of service registration function (`service` instruction) generated by the Aqua compiler
|
||||
*/
|
||||
export interface ServiceDef {
|
||||
/**
|
||||
* Default service id. If the service has no default id the value should be undefined
|
||||
*/
|
||||
defaultServiceId?: string;
|
||||
|
||||
/**
|
||||
* List of functions which the service consists of
|
||||
*/
|
||||
functions: LabeledProductType<ArrowWithoutCallbacks>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options to configure Aqua function execution
|
||||
*/
|
||||
export interface FnConfig {
|
||||
/**
|
||||
* Sets the TTL (time to live) for particle responsible for the function execution
|
||||
* If the option is not set the default TTL from FluencePeer config is used
|
||||
*/
|
||||
ttl?: number;
|
||||
}
|
||||
|
||||
export const getArgumentTypes = (
|
||||
def: FunctionCallDef,
|
||||
): {
|
||||
[key: string]: NonArrowType | ArrowWithoutCallbacks;
|
||||
} => {
|
||||
if (def.arrow.domain.tag !== 'labeledProduct') {
|
||||
throw new Error('Should be impossible');
|
||||
}
|
||||
|
||||
return def.arrow.domain.fields;
|
||||
};
|
||||
|
||||
export const isReturnTypeVoid = (def: FunctionCallDef): boolean => {
|
||||
if (def.arrow.codomain.tag === 'nil') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return def.arrow.codomain.items.length == 0;
|
||||
};
|
220
packages/core/interfaces/src/fluenceClient.ts
Normal file
220
packages/core/interfaces/src/fluenceClient.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import { SecurityTetraplet } from '@fluencelabs/avm';
|
||||
import type { LogLevel } from '@fluencelabs/marine-js/dist/types';
|
||||
import type { MultiaddrInput } from '@multiformats/multiaddr';
|
||||
import { FnConfig, FunctionCallDef, ServiceDef } from './compilerSupport.js';
|
||||
|
||||
/**
|
||||
* Peer ID's id as a base58 string (multihash/CIDv0).
|
||||
*/
|
||||
export type PeerIdB58 = string;
|
||||
|
||||
/**
|
||||
* Additional information about a service call
|
||||
* @typeparam ArgName
|
||||
*/
|
||||
export interface CallParams<ArgName extends string | null> {
|
||||
/**
|
||||
* The identifier of particle which triggered the call
|
||||
*/
|
||||
particleId: string;
|
||||
|
||||
/**
|
||||
* The peer id which created the particle
|
||||
*/
|
||||
initPeerId: PeerIdB58;
|
||||
|
||||
/**
|
||||
* Particle's timestamp when it was created
|
||||
*/
|
||||
timestamp: number;
|
||||
|
||||
/**
|
||||
* Time to live in milliseconds. The time after the particle should be expired
|
||||
*/
|
||||
ttl: number;
|
||||
|
||||
/**
|
||||
* Particle's signature
|
||||
*/
|
||||
signature?: string;
|
||||
|
||||
/**
|
||||
* Security tetraplets
|
||||
*/
|
||||
tetraplets: ArgName extends string ? Record<ArgName, SecurityTetraplet[]> : Record<string, never>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Node of the Fluence network specified as a pair of node's multiaddr and it's peer id
|
||||
*/
|
||||
type Node = {
|
||||
peerId: PeerIdB58;
|
||||
multiaddr: string;
|
||||
};
|
||||
|
||||
export type RelayOptions = string | MultiaddrInput | Node;
|
||||
|
||||
export type KeyTypes = 'RSA' | 'Ed25519' | 'secp256k1';
|
||||
|
||||
export type KeyPairOptions = {
|
||||
type: 'Ed25519';
|
||||
source: 'random' | Uint8Array;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration used when initiating Fluence Client
|
||||
*/
|
||||
export interface ClientOptions {
|
||||
/**
|
||||
* Node in Fluence network to connect to.
|
||||
* Can be in the form of:
|
||||
* - string: multiaddr in string format
|
||||
* - Multiaddr: multiaddr object, @see https://github.com/multiformats/js-multiaddr
|
||||
* - Node: node structure, @see Node
|
||||
* - Implementation of FluenceConnection class, @see FluenceConnection
|
||||
* If not specified the will work locally and would not be able to send or receive particles.
|
||||
*/
|
||||
relay?: RelayOptions;
|
||||
|
||||
/**
|
||||
* Specify the KeyPair to be used to identify the Fluence Peer.
|
||||
* Will be generated randomly if not specified
|
||||
*/
|
||||
keyPair?: KeyPairOptions;
|
||||
|
||||
connectionOptions?: {
|
||||
/**
|
||||
* When the peer established the connection to the network it sends a ping-like message to check if it works correctly.
|
||||
* The options allows to specify the timeout for that message in milliseconds.
|
||||
* If not specified the default timeout will be used
|
||||
CallParams,
|
||||
*/
|
||||
skipCheckConnection?: boolean;
|
||||
|
||||
/**
|
||||
* The dialing timeout in milliseconds
|
||||
*/
|
||||
dialTimeoutMs?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the default TTL for all particles originating from the peer with no TTL specified.
|
||||
* If the originating particle's TTL is defined then that value will be used
|
||||
* If the option is not set default TTL will be 7000
|
||||
*/
|
||||
defaultTtlMs?: number;
|
||||
|
||||
/**
|
||||
* Enables\disabled various debugging features
|
||||
*/
|
||||
debug?: {
|
||||
/**
|
||||
* If set to true, newly initiated particle ids will be printed to console.
|
||||
* Useful to see what particle id is responsible for aqua function
|
||||
*/
|
||||
printParticleId?: boolean;
|
||||
/**
|
||||
* Log level for marine services. By default logging is turned off.
|
||||
*/
|
||||
marineLogLevel?: LogLevel;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about Fluence Peer connection.
|
||||
* Represented as object with the following keys:
|
||||
* - `isInitialized`: Is the peer initialized or not.
|
||||
* - `peerId`: Peer Id of the peer. Null if the peer is not initialized
|
||||
* - `isConnected`: Is the peer connected to network or not
|
||||
* - `relayPeerId`: Peer Id of the relay the peer is connected to. If the connection is direct relayPeerId is null
|
||||
* - `isDirect`: True if the peer is connected to the network directly (not through relay)
|
||||
*/
|
||||
export type PeerStatus =
|
||||
| {
|
||||
isInitialized: false;
|
||||
peerId: null;
|
||||
isConnected: false;
|
||||
relayPeerId: null;
|
||||
}
|
||||
| {
|
||||
isInitialized: true;
|
||||
peerId: PeerIdB58;
|
||||
isConnected: false;
|
||||
relayPeerId: null;
|
||||
}
|
||||
| {
|
||||
isInitialized: true;
|
||||
peerId: PeerIdB58;
|
||||
isConnected: true;
|
||||
relayPeerId: PeerIdB58;
|
||||
}
|
||||
| {
|
||||
isInitialized: true;
|
||||
peerId: PeerIdB58;
|
||||
isConnected: true;
|
||||
isDirect: true;
|
||||
relayPeerId: null;
|
||||
};
|
||||
|
||||
export interface IFluenceClient {
|
||||
/**
|
||||
* Get the peer's status
|
||||
*/
|
||||
start(config?: ClientOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Un-initializes the peer: stops all the underlying workflows, stops the Aqua VM and disconnects from the Fluence network
|
||||
*/
|
||||
stop(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the peer's status
|
||||
*/
|
||||
getStatus(): PeerStatus;
|
||||
|
||||
/**
|
||||
* Return peers SK
|
||||
*/
|
||||
getSk(): Uint8Array;
|
||||
|
||||
// TODO: come up with a working interface for
|
||||
// - particle creation
|
||||
// - particle initialization
|
||||
// - service registration
|
||||
internals: any;
|
||||
|
||||
// TODO: extract this out of Client interface
|
||||
compilerSupport: {
|
||||
callFunction: (args: CallFunctionArgs) => Promise<unknown>;
|
||||
registerService: (args: RegisterServiceArgs) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CallFunctionArgs {
|
||||
def: FunctionCallDef;
|
||||
script: string;
|
||||
config: FnConfig;
|
||||
args: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface RegisterServiceArgs {
|
||||
def: ServiceDef;
|
||||
serviceId: string | undefined;
|
||||
service: any;
|
||||
}
|
||||
|
||||
export const asFluencePeer = (fluencePeerCandidate: unknown): IFluenceClient => {
|
||||
if (isFluencePeer(fluencePeerCandidate)) {
|
||||
return fluencePeerCandidate;
|
||||
}
|
||||
|
||||
throw new Error(`Argument ${fluencePeerCandidate} is not a Fluence Peer`);
|
||||
};
|
||||
|
||||
export const isFluencePeer = (fluencePeerCandidate: unknown): fluencePeerCandidate is IFluenceClient => {
|
||||
if (fluencePeerCandidate && (fluencePeerCandidate as any).__isFluenceAwesome) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
2
packages/core/interfaces/src/index.ts
Normal file
2
packages/core/interfaces/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './compilerSupport.js'
|
||||
export * from './fluenceClient.js'
|
8
packages/core/interfaces/tsconfig.json
Normal file
8
packages/core/interfaces/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
21
packages/core/js-peer/.gitignore
vendored
Normal file
21
packages/core/js-peer/.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
bundle/
|
||||
|
||||
dist
|
||||
esm
|
||||
types
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
.idea
|
12
packages/core/js-peer/.npmignore
Normal file
12
packages/core/js-peer/.npmignore
Normal file
@ -0,0 +1,12 @@
|
||||
.idea
|
||||
.gitignore
|
||||
node_modules
|
||||
types
|
||||
|
||||
src/
|
||||
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
|
||||
bundle
|
||||
pkg
|
1
packages/core/js-peer/.prettierignore
Normal file
1
packages/core/js-peer/.prettierignore
Normal file
@ -0,0 +1 @@
|
||||
/dist/
|
13
packages/core/js-peer/CONTRIBUTING.md
Normal file
13
packages/core/js-peer/CONTRIBUTING.md
Normal file
@ -0,0 +1,13 @@
|
||||
## Contribute Code
|
||||
|
||||
You are welcome to contribute to Fluence.
|
||||
|
||||
Things you need to know:
|
||||
|
||||
1. You need to **agree to the Contributors License Agreement**. This is a common practice in all major Open Source projects. At the current moment we are unable to accept contributions made on behalf of a company. Only individual contributions will be accepted.
|
||||
2. **Not all proposed contributions can be accepted**. Some features may e.g. just fit a third-party add-on better. The contribution must fit the overall direction of Fluence and really improve it. The more effort you invest, the better you should clarify in advance whether the contribution fits: the best way would be to just open an issue to discuss the contribution you plan to make.
|
||||
|
||||
### Contributor License Agreement
|
||||
|
||||
When you contribute, you have to be aware that your contribution is covered by **Apache License 2.0**, but might relicensed under few other software licenses mentioned in the **Contributor License Agreement**.
|
||||
In particular you need to agree to the [Contributor License Agreement](https://gist.github.com/fluencelabs-org/3f4cbb3cc14c1c0fb9ad99d8f7316ed7). If you agree to its content, you simply have to click on the link posted by the CLA assistant as a comment to the pull request. Click it to check the CLA, then accept it on the following screen if you agree to it. CLA assistant will save this decision for upcoming contributions and will notify you if there is any change to the CLA in the meantime.
|
11
packages/core/js-peer/README.md
Normal file
11
packages/core/js-peer/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# JS Peer
|
||||
|
||||
TDB
|
||||
|
||||
## Contributing
|
||||
|
||||
While the project is still in the early stages of development, you are welcome to track progress and contribute. As the project is undergoing rapid changes, interested contributors should contact the team before embarking on larger pieces of work. All contributors should consult with and agree to our [basic contributing rules](CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0](LICENSE)
|
12
packages/core/js-peer/aqua/node-utils.aqua
Normal file
12
packages/core/js-peer/aqua/node-utils.aqua
Normal file
@ -0,0 +1,12 @@
|
||||
data ReadFileResult:
|
||||
-- Was the call successful or not
|
||||
success: bool
|
||||
-- File content in base64 if the call was successful
|
||||
content: ?string
|
||||
-- Error message if the call was unsuccessful
|
||||
error: ?string
|
||||
|
||||
service NodeUtils("node_utils"):
|
||||
-- Read file from file system.
|
||||
-- returns file content in base64 format
|
||||
read_file(path: string) -> ReadFileResult
|
35
packages/core/js-peer/aqua/services.aqua
Normal file
35
packages/core/js-peer/aqua/services.aqua
Normal file
@ -0,0 +1,35 @@
|
||||
-- import SignResult, Sig from "@fluencelabs/aqua-lib/builtin.aqua"
|
||||
-- export SignResult, Sig
|
||||
|
||||
-- TODO:: fix this issue: https://github.com/fluencelabs/aqua-lib/issues/12
|
||||
-- and remove copy-paste
|
||||
|
||||
data SignResult:
|
||||
-- Was call successful or not
|
||||
success: bool
|
||||
-- Error message. Will be null if the call is successful
|
||||
error: ?string
|
||||
-- Signature as byte array. Will be null if the call is not successful
|
||||
signature: ?[]u8
|
||||
|
||||
-- Available only on FluenceJS peers
|
||||
-- The service can also be resolved by it's host peer id
|
||||
service Sig("sig"):
|
||||
-- Signs data with the service's private key.
|
||||
-- Depending on implementation the service might check call params to restrict usage for security reasons.
|
||||
-- By default it is only allowed to be used on the same peer the particle was initiated
|
||||
-- and accepts data only from the following sources:
|
||||
-- trust-graph.get_trust_bytes
|
||||
-- trust-graph.get_revocation_bytes
|
||||
-- registry.get_route_bytes
|
||||
-- registry.get_record_bytes
|
||||
-- registry.get_host_record_bytes
|
||||
-- Argument: data - byte array to sign
|
||||
-- Returns: signature as SignResult structure
|
||||
sign(data: []u8) -> SignResult
|
||||
|
||||
-- Given the data and signature both as byte arrays, returns true if the signature is correct, false otherwise.
|
||||
verify(signature: []u8, data: []u8) -> bool
|
||||
|
||||
-- Gets service's public key.
|
||||
get_peer_id() -> string
|
32
packages/core/js-peer/aqua/single-module-srv.aqua
Normal file
32
packages/core/js-peer/aqua/single-module-srv.aqua
Normal file
@ -0,0 +1,32 @@
|
||||
alias Bytes : []u8
|
||||
|
||||
data ServiceCreationResult:
|
||||
success: bool
|
||||
service_id: ?string
|
||||
error: ?string
|
||||
|
||||
data ReadFileResult:
|
||||
success: bool
|
||||
content: ?string
|
||||
error: ?string
|
||||
|
||||
data RemoveResult:
|
||||
success: bool
|
||||
error: ?string
|
||||
|
||||
alias ListServiceResult: []string
|
||||
|
||||
service Srv("single_module_srv"):
|
||||
-- Used to create a service on a certain node
|
||||
-- Arguments:
|
||||
-- bytes – a base64 string containing the .wasm module to add.
|
||||
-- Returns: service_id – the service ID of the created service.
|
||||
create(wasm_b64_content: string) -> ServiceCreationResult
|
||||
|
||||
-- Used to remove a service from a certain node
|
||||
-- Arguments:
|
||||
-- service_id – ID of the service to remove
|
||||
remove(service_id: string) -> RemoveResult
|
||||
|
||||
-- Returns a list of services ids running on a peer
|
||||
list() -> ListServiceResult
|
16
packages/core/js-peer/jest.config.cjs
Normal file
16
packages/core/js-peer/jest.config.cjs
Normal file
@ -0,0 +1,16 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||
},
|
||||
testPathIgnorePatterns: ['dist'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
65
packages/core/js-peer/package.json
Normal file
65
packages/core/js-peer/package.json
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "@fluencelabs/js-peer",
|
||||
"version": "0.5.0",
|
||||
"description": "TypeScript implementation of Fluence Peer",
|
||||
"main": "./dist/index.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"pnpm": ">=3"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"compile-aqua": "aqua -i ./aqua/ -o ./src/internal/_aqua",
|
||||
"test:smoke": "node ./dist/js-peer/__test__/integration/smokeTest.js",
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules pnpm jest",
|
||||
"test:unit": "NODE_OPTIONS=--experimental-vm-modules pnpm jest --testPathPattern=src/__test__/unit",
|
||||
"test:integration": "NODE_OPTIONS=--experimental-vm-modules pnpm jest --testPathPattern=src/__test__/integration"
|
||||
},
|
||||
"repository": "https://github.com/fluencelabs/fluence-js",
|
||||
"author": "Fluence Labs",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fluencelabs/interfaces": "0.5.0",
|
||||
"@fluencelabs/avm": "0.31.10",
|
||||
"@fluencelabs/marine-js": "0.3.44",
|
||||
"multiformats": "11.0.1",
|
||||
"async": "3.2.4",
|
||||
"bs58": "5.0.0",
|
||||
"buffer": "6.0.3",
|
||||
"loglevel": "1.8.1",
|
||||
"@libp2p/peer-id": "2.0.1",
|
||||
"rxjs": "7.5.5",
|
||||
"ts-pattern": "3.3.3",
|
||||
"uuid": "8.3.2",
|
||||
"threads": "1.7.0",
|
||||
"@libp2p/crypto": "1.0.8",
|
||||
"@libp2p/peer-id-factory": "2.0.1",
|
||||
"@libp2p/interface-peer-id": "2.0.1",
|
||||
"@libp2p/interface-keys": "1.0.7",
|
||||
"js-base64": "3.7.2",
|
||||
"it-length-prefixed": "8.0.4",
|
||||
"it-pipe": "2.0.5",
|
||||
"it-map": "2.0.0",
|
||||
"uint8arrays": "4.0.3",
|
||||
"@chainsafe/libp2p-noise": "11.0.0",
|
||||
"libp2p": "0.42.2",
|
||||
"@libp2p/interfaces": "3.3.1",
|
||||
"@libp2p/interface-connection": "3.0.8",
|
||||
"@libp2p/mplex": "7.1.1",
|
||||
"@libp2p/websockets": "5.0.3",
|
||||
"@multiformats/multiaddr": "11.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fluencelabs/aqua": "0.7.7-362",
|
||||
"@fluencelabs/aqua-api": "0.9.1-373",
|
||||
"@fluencelabs/aqua-lib": "0.6.0",
|
||||
"@fluencelabs/fluence-network-environment": "1.0.13",
|
||||
"@types/bs58": "4.0.1",
|
||||
"@types/uuid": "8.3.2",
|
||||
"@types/jest": "29.4.0",
|
||||
"jest": "29.4.1",
|
||||
"ts-jest": "29.0.5"
|
||||
}
|
||||
}
|
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);
|
||||
}
|
||||
}
|
9
packages/core/js-peer/tsconfig.json
Normal file
9
packages/core/js-peer/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
Reference in New Issue
Block a user