mirror of
https://github.com/fluencelabs/fluence-js.git
synced 2025-06-03 19:21:19 +00:00
559 lines
14 KiB
TypeScript
559 lines
14 KiB
TypeScript
/**
|
|
* Copyright 2023 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 } from "buffer";
|
|
|
|
import { JSONValue } from "@fluencelabs/interfaces";
|
|
import bs58 from "bs58";
|
|
import { sha256 } from "multiformats/hashes/sha2";
|
|
import { z } from "zod";
|
|
|
|
import {
|
|
CallServiceData,
|
|
CallServiceResult,
|
|
CallServiceResultType,
|
|
GenericCallServiceHandler,
|
|
ResultCodes,
|
|
} from "../jsServiceHost/interfaces.js";
|
|
import { getErrorMessage, jsonify } from "../util/utils.js";
|
|
|
|
const success = (result: CallServiceResultType): CallServiceResult => {
|
|
return {
|
|
result,
|
|
retCode: ResultCodes.success,
|
|
};
|
|
};
|
|
|
|
const error = (error: CallServiceResultType): CallServiceResult => {
|
|
return {
|
|
result: error,
|
|
retCode: ResultCodes.error,
|
|
};
|
|
};
|
|
|
|
const chunk = <T>(arr: T[]): T[][] => {
|
|
const res: T[][] = [];
|
|
const chunkSize = 2;
|
|
|
|
for (let i = 0; i < arr.length; i += chunkSize) {
|
|
const chunk = arr.slice(i, i + chunkSize);
|
|
res.push(chunk);
|
|
}
|
|
|
|
return res;
|
|
};
|
|
|
|
const errorNotImpl = (methodName: string) => {
|
|
return error(
|
|
`The JS implementation of Peer does not support "${methodName}"`,
|
|
);
|
|
};
|
|
|
|
const parseWithSchema = <T extends z.ZodTypeAny>(
|
|
schema: T,
|
|
req: CallServiceData,
|
|
): [z.infer<T>, null] | [null, string] => {
|
|
const result = schema.safeParse(req.args, {
|
|
errorMap: (issue, ctx) => {
|
|
if (
|
|
issue.code === z.ZodIssueCode.invalid_type &&
|
|
issue.path.length === 1 &&
|
|
typeof issue.path[0] === "number"
|
|
) {
|
|
const [arg] = issue.path;
|
|
return {
|
|
message: `Argument ${arg} expected to be of type ${issue.expected}, Got ${issue.received}`,
|
|
};
|
|
}
|
|
|
|
if (issue.code === z.ZodIssueCode.too_big) {
|
|
return {
|
|
message: `Expected ${
|
|
issue.maximum
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
} argument(s). Got ${ctx.data.length}`,
|
|
};
|
|
}
|
|
|
|
if (issue.code === z.ZodIssueCode.too_small) {
|
|
return {
|
|
message: `Expected ${
|
|
issue.minimum
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
} argument(s). Got ${ctx.data.length}`,
|
|
};
|
|
}
|
|
|
|
if (issue.code === z.ZodIssueCode.invalid_union) {
|
|
return {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
message: `Expected argument(s). Got ${ctx.data.length}`,
|
|
};
|
|
}
|
|
|
|
return { message: ctx.defaultError };
|
|
},
|
|
});
|
|
|
|
if (result.success) {
|
|
return [result.data, null];
|
|
} else {
|
|
return [null, result.error.errors[0].message];
|
|
}
|
|
};
|
|
|
|
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
type Literal = z.infer<typeof literalSchema>;
|
|
type Json = Literal | { [key: string]: Json } | Json[];
|
|
|
|
const jsonSchema: z.ZodType<Json> = z.lazy(() => {
|
|
return z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]);
|
|
});
|
|
|
|
const jsonImplSchema = z
|
|
.tuple([z.record(jsonSchema)])
|
|
.rest(z.tuple([z.string(), jsonSchema]));
|
|
|
|
const makeJsonImpl = (args: z.infer<typeof jsonImplSchema>) => {
|
|
const [obj, ...kvs] = args;
|
|
return success({ ...obj, ...Object.fromEntries(kvs) });
|
|
};
|
|
|
|
type withSchema = <T extends z.ZodTypeAny>(
|
|
arg: T,
|
|
) => (
|
|
arg1: (value: z.infer<T>) => CallServiceResult | Promise<CallServiceResult>,
|
|
) => (req: CallServiceData) => CallServiceResult | Promise<CallServiceResult>;
|
|
|
|
const withSchema: withSchema = <T extends z.ZodTypeAny>(schema: T) => {
|
|
return (bound) => {
|
|
return (req) => {
|
|
const [value, message] = parseWithSchema(schema, req);
|
|
|
|
if (message != null) {
|
|
return error(message);
|
|
}
|
|
|
|
return bound(value);
|
|
};
|
|
};
|
|
};
|
|
|
|
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: withSchema(z.tuple([z.number(), z.string()]))(
|
|
([durationMs, msg]) => {
|
|
return new Promise((resolve) => {
|
|
setTimeout(() => {
|
|
const res = success(msg);
|
|
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: withSchema(z.tuple([z.array(z.unknown())]))(([arr]) => {
|
|
return success(arr.length);
|
|
}),
|
|
|
|
identity: withSchema(z.array(jsonSchema).max(1))((args) => {
|
|
return success(args.length === 0 ? {} : args[0]);
|
|
}),
|
|
|
|
concat: withSchema(z.array(z.array(z.unknown())))((args) => {
|
|
// Schema is used with unknown type to prevent useless runtime check
|
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
const arr = args as never[][];
|
|
|
|
return success(arr.flat());
|
|
}),
|
|
|
|
string_to_b58: withSchema(z.tuple([z.string()]))(([input]) => {
|
|
return success(bs58.encode(new TextEncoder().encode(input)));
|
|
}),
|
|
|
|
string_from_b58: withSchema(z.tuple([z.string()]))(([input]) => {
|
|
return success(new TextDecoder().decode(bs58.decode(input)));
|
|
}),
|
|
|
|
bytes_to_b58: withSchema(z.tuple([z.array(z.number())]))(([input]) => {
|
|
return success(bs58.encode(new Uint8Array(input)));
|
|
}),
|
|
|
|
bytes_from_b58: withSchema(z.tuple([z.string()]))(([input]) => {
|
|
return success(Array.from(bs58.decode(input)));
|
|
}),
|
|
|
|
sha256_string: withSchema(z.tuple([z.string()]))(async ([input]) => {
|
|
const inBuffer = Buffer.from(input);
|
|
const multihash = await sha256.digest(inBuffer);
|
|
|
|
return success(bs58.encode(multihash.bytes));
|
|
}),
|
|
|
|
concat_strings: withSchema(z.array(z.string()))((args) => {
|
|
return success(args.join(""));
|
|
}),
|
|
},
|
|
|
|
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: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
|
|
return success(x + y);
|
|
}),
|
|
|
|
sub: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
|
|
return success(x - y);
|
|
}),
|
|
|
|
mul: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
|
|
return success(x * y);
|
|
}),
|
|
|
|
fmul: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
|
|
return success(Math.floor(x * y));
|
|
}),
|
|
|
|
div: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
|
|
return success(Math.floor(x / y));
|
|
}),
|
|
|
|
rem: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
|
|
return success(x % y);
|
|
}),
|
|
|
|
pow: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
|
|
return success(Math.pow(x, y));
|
|
}),
|
|
|
|
log: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
|
|
return success(Math.log(y) / Math.log(x));
|
|
}),
|
|
},
|
|
|
|
cmp: {
|
|
gt: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
|
|
return success(x > y);
|
|
}),
|
|
|
|
gte: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
|
|
return success(x >= y);
|
|
}),
|
|
|
|
lt: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
|
|
return success(x < y);
|
|
}),
|
|
|
|
lte: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
|
|
return success(x <= y);
|
|
}),
|
|
|
|
cmp: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
|
|
return success(x === y ? 0 : x > y ? 1 : -1);
|
|
}),
|
|
},
|
|
|
|
array: {
|
|
sum: withSchema(z.tuple([z.array(z.number())]))(([xs]) => {
|
|
return success(
|
|
xs.reduce((agg, cur) => {
|
|
return agg + cur;
|
|
}, 0),
|
|
);
|
|
}),
|
|
|
|
dedup: withSchema(z.tuple([z.array(z.any())]))(([xs]) => {
|
|
const set = new Set(xs);
|
|
return success(Array.from(set));
|
|
}),
|
|
|
|
intersect: withSchema(z.tuple([z.array(z.any()), z.array(z.any())]))(
|
|
([xs, ys]) => {
|
|
const intersection = xs.filter((x) => {
|
|
return ys.includes(x);
|
|
});
|
|
|
|
return success(intersection);
|
|
},
|
|
),
|
|
|
|
diff: withSchema(z.tuple([z.array(z.any()), z.array(z.any())]))(
|
|
([xs, ys]) => {
|
|
const diff = xs.filter((x) => {
|
|
return !ys.includes(x);
|
|
});
|
|
|
|
return success(diff);
|
|
},
|
|
),
|
|
|
|
sdiff: withSchema(z.tuple([z.array(z.any()), z.array(z.any())]))(
|
|
([xs, ys]) => {
|
|
const sdiff = [
|
|
xs.filter((y) => {
|
|
return !ys.includes(y);
|
|
}),
|
|
ys.filter((x) => {
|
|
return !xs.includes(x);
|
|
}),
|
|
].flat();
|
|
|
|
return success(sdiff);
|
|
},
|
|
),
|
|
},
|
|
|
|
json: {
|
|
obj: withSchema(
|
|
z
|
|
.array(z.unknown())
|
|
.refine(
|
|
(arr) => {
|
|
return arr.length % 2 === 0;
|
|
},
|
|
(arr) => {
|
|
return {
|
|
message: "Expected even number of argument(s). Got " + arr.length,
|
|
};
|
|
},
|
|
)
|
|
.transform((args) => {
|
|
return chunk(args);
|
|
})
|
|
.pipe(z.array(z.tuple([z.string(), jsonSchema]))),
|
|
)((args) => {
|
|
return makeJsonImpl([{}, ...args]);
|
|
}),
|
|
|
|
put: withSchema(
|
|
z
|
|
.tuple([z.record(jsonSchema), z.string(), jsonSchema])
|
|
.transform(
|
|
([obj, name, value]): [{ [key: string]: Json }, [string, Json]] => {
|
|
return [obj, [name, value]];
|
|
},
|
|
),
|
|
)(makeJsonImpl),
|
|
|
|
puts: withSchema(
|
|
z
|
|
.array(z.unknown())
|
|
.refine(
|
|
(arr) => {
|
|
return arr.length >= 3;
|
|
},
|
|
(value) => {
|
|
return {
|
|
message: `Expected more than 3 argument(s). Got ${value.length}`,
|
|
};
|
|
},
|
|
)
|
|
.refine(
|
|
(arr) => {
|
|
return arr.length % 2 === 1;
|
|
},
|
|
{
|
|
message: "Argument count must be odd.",
|
|
},
|
|
)
|
|
.transform((args) => {
|
|
return [args[0], ...chunk(args.slice(1))];
|
|
})
|
|
.pipe(jsonImplSchema),
|
|
)(makeJsonImpl),
|
|
|
|
stringify: withSchema(z.tuple([z.record(z.string(), jsonSchema)]))(
|
|
([json]) => {
|
|
const res = JSON.stringify(json);
|
|
return success(res);
|
|
},
|
|
),
|
|
|
|
parse: withSchema(z.tuple([z.string()]))(([raw]) => {
|
|
try {
|
|
// Parsing any argument here yields JSONValue
|
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
const json = JSON.parse(raw) as JSONValue;
|
|
return success(json);
|
|
} catch (err: unknown) {
|
|
return error(getErrorMessage(err));
|
|
}
|
|
}),
|
|
},
|
|
|
|
"run-console": {
|
|
print: (req) => {
|
|
// This log is intentional
|
|
// eslint-disable-next-line no-console
|
|
console.log(...req.args);
|
|
return success({});
|
|
},
|
|
},
|
|
} as const;
|