/** * 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 = (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 = ( schema: T, req: CallServiceData, ): [z.infer, 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; type Json = Literal | { [key: string]: Json } | Json[]; const jsonSchema: z.ZodType = 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) => { const [obj, ...kvs] = args; return success({ ...obj, ...Object.fromEntries(kvs) }); }; type withSchema = ( arg: T, ) => ( arg1: (value: z.infer) => CallServiceResult | Promise, ) => (req: CallServiceData) => CallServiceResult | Promise; const withSchema: withSchema = (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 > = { 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 = ""; } 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;