From 53385c556a6672e260b1a722c8ea0e11250d4fb3 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 3 Jun 2021 20:11:09 +0300 Subject: [PATCH] Implement handlers for built-in calls from avm (#52) --- package-lock.json | 127 +++++++++++++++++++++-- package.json | 2 + src/FluenceClient.ts | 6 +- src/__test__/integration/client.spec.ts | 4 +- src/__test__/unit/builtInHandler.spec.ts | 60 +++++++++++ src/internal/CallServiceHandler.ts | 4 +- src/internal/ClientImpl.ts | 12 +-- src/internal/defaultClientHandler.ts | 97 +++++++++++++++++ tsconfig.json | 1 - types/ipfs-only-hash/index.d.ts | 19 ---- types/it-length-prefixed/index.d.ts | 20 ---- 11 files changed, 289 insertions(+), 63 deletions(-) create mode 100644 src/__test__/unit/builtInHandler.spec.ts create mode 100644 src/internal/defaultClientHandler.ts delete mode 100644 types/ipfs-only-hash/index.d.ts delete mode 100644 types/it-length-prefixed/index.d.ts diff --git a/package-lock.json b/package-lock.json index eaa1e4f1..cf957b50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -842,6 +842,15 @@ "@types/node": "*" } }, + "@types/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-yfAgiWgVLjFCmRv8zAcOIHywYATEwiTVccTLnRp6UxTNavT55M9d/uhK3T03St/+8/z/wW+CRjGKUNmEqoHHCA==", + "dev": true, + "requires": { + "base-x": "^3.0.6" + } + }, "@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -3500,16 +3509,105 @@ } }, "jest-each": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz", - "integrity": "sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A==", + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.0.2.tgz", + "integrity": "sha512-OLMBZBZ6JkoXgUenDtseFRWA43wVl2BwmZYIWQws7eS7pqsIvePqj/jJmEnfq91ALk3LNphgwNK/PRFBYi7ITQ==", "dev": true, "requires": { - "@jest/types": "^26.6.2", + "@jest/types": "^27.0.2", "chalk": "^4.0.0", - "jest-get-type": "^26.3.0", - "jest-util": "^26.6.2", - "pretty-format": "^26.6.2" + "jest-get-type": "^27.0.1", + "jest-util": "^27.0.2", + "pretty-format": "^27.0.2" + }, + "dependencies": { + "@jest/types": { + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.2.tgz", + "integrity": "sha512-XpjCtJ/99HB4PmyJ2vgmN7vT+JLP7RW1FBT9RgnMFS4Dt7cvIyBee8O3/j98aUZ34ZpenPZFqmaaObWSeL65dg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.3.tgz", + "integrity": "sha512-YlFfTGS+zqCgXuXNV26rOIeETOkXnGQXP/pjjL9P0gO/EP9jTmc7pUBhx+jVEIxpq41RX33GQ7N3DzOSfZoglQ==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "ci-info": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.2.0.tgz", + "integrity": "sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "dev": true + }, + "is-ci": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.0.tgz", + "integrity": "sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ==", + "dev": true, + "requires": { + "ci-info": "^3.1.1" + } + }, + "jest-get-type": { + "version": "27.0.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.1.tgz", + "integrity": "sha512-9Tggo9zZbu0sHKebiAijyt1NM77Z0uO4tuWOxUCujAiSeXv30Vb5D4xVF4UR4YWNapcftj+PbByU54lKD7/xMg==", + "dev": true + }, + "jest-util": { + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.0.2.tgz", + "integrity": "sha512-1d9uH3a00OFGGWSibpNYr+jojZ6AckOMCXV2Z4K3YXDnzpkAaXQyIpY14FOJPiUmil7CD+A6Qs+lnnh6ctRbIA==", + "dev": true, + "requires": { + "@jest/types": "^27.0.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^3.0.0", + "picomatch": "^2.2.3" + } + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + }, + "pretty-format": { + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.2.tgz", + "integrity": "sha512-mXKbbBPnYTG7Yra9qFBtqj+IXcsvxsvOBco3QHxtxTl+hHKq6QdzMZ+q0CtL4ORHZgwGImRr2XZUX2EWzORxig==", + "dev": true, + "requires": { + "@jest/types": "^27.0.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + } + } } }, "jest-environment-jsdom": { @@ -3601,6 +3699,21 @@ "jest-util": "^26.6.2", "pretty-format": "^26.6.2", "throat": "^5.0.0" + }, + "dependencies": { + "jest-each": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz", + "integrity": "sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "jest-get-type": "^26.3.0", + "jest-util": "^26.6.2", + "pretty-format": "^26.6.2" + } + } } }, "jest-leak-detector": { diff --git a/package.json b/package.json index 5d2506f8..8b345032 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,10 @@ "uuid": "8.3.0" }, "devDependencies": { + "@types/bs58": "^4.0.1", "@types/jest": "^26.0.22", "jest": "^26.6.3", + "jest-each": "^27.0.2", "ts-jest": "^26.5.4", "typescript": "^3.9.5" } diff --git a/src/FluenceClient.ts b/src/FluenceClient.ts index ce1f8f72..63889012 100644 --- a/src/FluenceClient.ts +++ b/src/FluenceClient.ts @@ -133,14 +133,14 @@ export const checkConnection = async (client: FluenceClient, ttl?: number): Prom .withVariables({ msg, }) - .buildAsFetch<[[string]]>(callbackService, callbackFn); + .buildAsFetch<[string]>(callbackService, callbackFn); await client.initiateFlow(request); try { - const [[result]] = await promise; + const [result] = await promise; if (result != msg) { - log.warn("unexpected behavior. 'identity' must return arguments the passed arguments."); + log.warn("unexpected behavior. 'identity' must return the passed arguments."); } return true; } catch (e) { diff --git a/src/__test__/integration/client.spec.ts b/src/__test__/integration/client.spec.ts index 70d486bb..871bf63e 100644 --- a/src/__test__/integration/client.spec.ts +++ b/src/__test__/integration/client.spec.ts @@ -26,11 +26,11 @@ describe('Typescript usage suite', () => { (call %init_peer_id% ("callback" "callback") [result]) )`, ) - .buildAsFetch<[[string]]>('callback', 'callback'); + .buildAsFetch<[string]>('callback', 'callback'); await client.initiateFlow(request); // assert - const [[result]] = await promise; + const [result] = await promise; expect(result).toBe('hello world!'); }); diff --git a/src/__test__/unit/builtInHandler.spec.ts b/src/__test__/unit/builtInHandler.spec.ts new file mode 100644 index 00000000..2563a56e --- /dev/null +++ b/src/__test__/unit/builtInHandler.spec.ts @@ -0,0 +1,60 @@ +import each from 'jest-each'; +import { CallServiceData } from '../../internal/CallServiceHandler'; +import makeDefaultClientHandler from '../../internal/defaultClientHandler'; + +describe('Tests for default handler', () => { + // prettier-ignore + each` + fnName | args | retCode | result + ${'identity'} | ${[]} | ${0} | ${{}} + ${'identity'} | ${[1]} | ${0} | ${1} + ${'identity'} | ${[1, 2]} | ${1} | ${'identity accepts up to 1 arguments, received 2 arguments'} + + ${'noop'} | ${[1, 2]} | ${0} | ${{}} + + ${'array'} | ${[1, 2, 3]} | ${0} | ${[1, 2, 3]} + + ${'concat'} | ${[[1, 2], [3, 4], [5, 6]]} | ${0} | ${[1, 2, 3, 4, 5, 6]} + ${'concat'} | ${[[1, 2]]} | ${0} | ${[1, 2]} + ${'concat'} | ${[]} | ${0} | ${[]} + ${'concat'} | ${[1, [1, 2], 1]} | ${1} | ${"All arguments of 'concat' must be arrays: arguments 0, 2 are not"} + + ${'string_to_b58'} | ${["test"]} | ${0} | ${"3yZe7d"} + ${'string_to_b58'} | ${["test", 1]} | ${1} | ${"string_to_b58 accepts only one string argument"} + + ${'string_from_b58'} | ${["3yZe7d"]} | ${0} | ${"test"} + ${'string_from_b58'} | ${["3yZe7d", 1]} | ${1} | ${"string_from_b58 accepts only one string argument"} + + ${'bytes_to_b58'} | ${[[116, 101, 115, 116]]} | ${0} | ${"3yZe7d"} + ${'bytes_to_b58'} | ${[[116, 101, 115, 116], 1]} | ${1} | ${"bytes_to_b58 accepts only single argument: array of numbers"} + + ${'bytes_from_b58'} | ${["3yZe7d"]} | ${0} | ${[116, 101, 115, 116]} + ${'bytes_from_b58'} | ${["3yZe7d", 1]} | ${1} | ${"bytes_from_b58 accepts only one string argument"} + +`.test( + // + '$fnName with $args expected retcode: $retCode and result: $result', + ({ fnName, args, retCode, result }) => { + // arrange + const req: CallServiceData = { + serviceId: 'Op', + fnName: fnName, + args: args, + tetraplets: [], + particleContext: { + particleId: 'some', + }, + }; + + // act + const res = makeDefaultClientHandler().execute(req); + + // assert + expect(res).toMatchObject({ + retCode: retCode, + result: result, + }); + const handler = makeDefaultClientHandler(); + }, + ); +}); diff --git a/src/internal/CallServiceHandler.ts b/src/internal/CallServiceHandler.ts index 563147bb..93c0e3cd 100644 --- a/src/internal/CallServiceHandler.ts +++ b/src/internal/CallServiceHandler.ts @@ -21,7 +21,7 @@ interface ParticleContext { /** * Represents the information passed from AVM when a `call` air instruction is executed on the local peer */ -interface CallServiceData { +export interface CallServiceData { /** * Service ID as specified in `call` air instruction */ @@ -58,7 +58,7 @@ export type CallServiceResultType = object | boolean | number | string; /** * Represents the result of the `call` air instruction to be returned into AVM */ -interface CallServiceResult { +export interface CallServiceResult { /** * Return code to be returned to AVM */ diff --git a/src/internal/ClientImpl.ts b/src/internal/ClientImpl.ts index 7960b4ca..de24f83f 100644 --- a/src/internal/ClientImpl.ts +++ b/src/internal/ClientImpl.ts @@ -21,18 +21,12 @@ import { FluenceConnection, FluenceConnectionOptions } from './FluenceConnection import { PeerIdB58 } from './commonTypes'; import { FluenceClient } from '../FluenceClient'; import { RequestFlow } from './RequestFlow'; -import { CallServiceHandler, errorHandler, fnHandler } from './CallServiceHandler'; +import { CallServiceHandler } from './CallServiceHandler'; import { loadRelayFn, loadVariablesService } from './RequestFlowBuilder'; import { logParticle, Particle } from './particle'; import log from 'loglevel'; -import { AirInterpreter, CallServiceResult, ParticleHandler, SecurityTetraplet } from '@fluencelabs/avm'; - -const makeDefaultClientHandler = (): CallServiceHandler => { - const res = new CallServiceHandler(); - res.use(errorHandler); - res.use(fnHandler('op', 'identity', (args, _) => args)); - return res; -}; +import { AirInterpreter, ParticleHandler, SecurityTetraplet, CallServiceResult } from '@fluencelabs/avm'; +import makeDefaultClientHandler from './defaultClientHandler'; export class ClientImpl implements FluenceClient { readonly selfPeerIdFull: PeerId; diff --git a/src/internal/defaultClientHandler.ts b/src/internal/defaultClientHandler.ts new file mode 100644 index 00000000..04608085 --- /dev/null +++ b/src/internal/defaultClientHandler.ts @@ -0,0 +1,97 @@ +import { encode, decode } from 'bs58'; +import { + CallServiceData, + CallServiceHandler, + CallServiceResult, + CallServiceResultType, + errorHandler, + Middleware, +} from './CallServiceHandler'; + +const makeDefaultClientHandler = (): CallServiceHandler => { + const success = (resp: CallServiceResult, result: CallServiceResultType) => { + resp.retCode = 0; + resp.result = result; + }; + const error = (resp: CallServiceResult, errorMsg: string) => { + resp.retCode = 1; + resp.result = errorMsg; + }; + const mw: Middleware = (req: CallServiceData, resp: CallServiceResult, next: Function) => { + if (req.serviceId === 'Op') { + switch (req.fnName) { + case 'noop': + success(resp, {}); + return; + + case 'array': + success(resp, req.args); + return; + + case 'identity': + if (req.args.length > 1) { + error(resp, `identity accepts up to 1 arguments, received ${req.args.length} arguments`); + } else { + success(resp, req.args.length === 0 ? {} : req.args[0]); + } + return; + + case 'concat': + 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(', '); + error(resp, `All arguments of 'concat' must be arrays: arguments ${str} are not`); + } else { + success(resp, [].concat.apply([], req.args)); + } + return; + + case 'string_to_b58': + if (req.args.length !== 1) { + error(resp, 'string_to_b58 accepts only one string argument'); + } else { + success(resp, encode(new TextEncoder().encode(req.args[0]))); + } + return; + + case 'string_from_b58': + if (req.args.length !== 1) { + error(resp, 'string_from_b58 accepts only one string argument'); + } else { + success(resp, new TextDecoder().decode(decode(req.args[0]))); + } + return; + + case 'bytes_to_b58': + if (req.args.length !== 1 || !Array.isArray(req.args[0])) { + error(resp, 'bytes_to_b58 accepts only single argument: array of numbers'); + } else { + const argumentArray = req.args[0] as number[]; + success(resp, encode(new Uint8Array(argumentArray))); + } + return; + + case 'bytes_from_b58': + if (req.args.length !== 1) { + error(resp, 'bytes_from_b58 accepts only one string argument'); + } else { + success(resp, Array.from(decode(req.args[0]))); + } + return; + } + } + + next(); + }; + + const res = new CallServiceHandler(); + res.use(errorHandler); + res.use(mw); + return res; +}; + +export default makeDefaultClientHandler; diff --git a/tsconfig.json b/tsconfig.json index c9d20ddc..fd1098fc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,6 @@ "typeRoots": [ "./node_modules/@types", "./node_modules/libp2p-ts/types", - "./types" ], "outDir": "./dist/", "baseUrl": ".", diff --git a/types/ipfs-only-hash/index.d.ts b/types/ipfs-only-hash/index.d.ts deleted file mode 100644 index 99e49e00..00000000 --- a/types/ipfs-only-hash/index.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2020 Fluence Labs Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -declare module 'ipfs-only-hash' { - export function of(data: Buffer): Promise -} diff --git a/types/it-length-prefixed/index.d.ts b/types/it-length-prefixed/index.d.ts deleted file mode 100644 index 9da68a31..00000000 --- a/types/it-length-prefixed/index.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 Fluence Labs Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -declare module 'it-length-prefixed' { - export function decode(): any - export function encode(): any -}