├── .github
└── workflows
│ ├── publish.yaml
│ └── tests.yaml
├── .gitignore
├── README.md
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── cli
│ ├── cmd
│ │ ├── em-wrapper.ts
│ │ └── functions
│ │ │ ├── deploy.ts
│ │ │ ├── evaluate
│ │ │ ├── app-simulator.ts
│ │ │ ├── arweave-evaluate.ts
│ │ │ ├── common.ts
│ │ │ ├── evaluate.ts
│ │ │ ├── fetch-bundle.ts
│ │ │ └── readers
│ │ │ │ ├── arweave
│ │ │ │ ├── arweave-reader.ts
│ │ │ │ └── graphql.ts
│ │ │ │ └── reader.interface.ts
│ │ │ ├── function-test.ts
│ │ │ ├── model.ts
│ │ │ ├── read.ts
│ │ │ └── write.ts
│ ├── index.ts
│ └── utils
│ │ └── common.ts
├── common
│ ├── em.ts
│ ├── exceptions
│ │ ├── deployOpFailure.ts
│ │ ├── readFailure.ts
│ │ └── writeOpFailure.ts
│ ├── functions
│ │ ├── deploy.ts
│ │ ├── read.ts
│ │ └── write.ts
│ ├── model.ts
│ ├── utils
│ │ └── commons.ts
│ └── vars.ts
├── index.ts
└── test
│ └── test-function.ts
├── tests
├── cli-function.tests.ts
└── testdata
│ ├── user-registry-init.json
│ └── user-registry.js
├── tsconfig.build.json
└── tsconfig.json
/.github/workflows/publish.yaml:
--------------------------------------------------------------------------------
1 | name: Publishing Package
2 | on:
3 | release:
4 | types: [created]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | # Setup .npmrc file to publish to npm
11 | - uses: actions/setup-node@v2
12 | with:
13 | node-version: '16.x'
14 | registry-url: 'https://registry.npmjs.org'
15 | - run: npm install
16 | - run: npm install -g typescript
17 | - run: npm run deliver
18 | env:
19 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
1 | name: "CI"
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - dev
7 | - main
8 | - master
9 | - "releases/*"
10 |
11 | jobs:
12 | # unit tests
13 | tests:
14 | name: "Unit tests"
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v2
18 | - run: npm install
19 | - run: npm run test
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | pnpm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 |
14 | # OS
15 | .DS_Store
16 |
17 | # Tests
18 | /coverage
19 | /.nyc_output
20 |
21 | # IDEs and editors
22 | /.idea
23 | .project
24 | .classpath
25 | .c9/
26 | *.launch
27 | .settings/
28 | *.sublime-workspace
29 |
30 | # IDE - VSCode
31 | .vscode/*
32 | !.vscode/settings.json
33 | !.vscode/tasks.json
34 | !.vscode/launch.json
35 | !.vscode/extensions.json
36 | .env
37 | /ar-local/wallet.json
38 | **/wallet.json
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Execution Machine JS SDK
4 |
5 |
6 | A Javascript SDK to interact with The Execution Machine (EXM)
7 |
8 |
9 |
10 | ## General
11 |
12 | EXM can be used through a JS SDK in order to perform operations with the platform. To install the SDK.
13 |
14 | ```shell
15 | $ npm i @execution-machine/sdk
16 | ```
17 |
18 | After installing the npm package as described above. You can initialize EXM by doing the following
19 |
20 | ```javascript
21 | // import { Exm } from '@execution-machine/sdk'
22 | const { Exm } = require('@execution-machine/sdk');
23 |
24 | const exmInstance = new Exm({ token: 'MY_EXM_TOKEN' });
25 | ```
26 |
27 | ## Docs
28 |
29 | Documentation for SDK is available online. Please [Click here](https://docs.exm.dev/trustless-serverless-functions/introduction/js-sdk) for more information.
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
2 | module.exports = {
3 | preset: 'ts-jest',
4 | testEnvironment: 'node',
5 | testMatch: [
6 | "**/tests/**/*.tests.ts"
7 | ]
8 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@execution-machine/sdk",
3 | "version": "0.1.9",
4 | "description": "",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "build": "tsc",
9 | "test": "jest",
10 | "deliver": "npm run build && npm publish --access public"
11 | },
12 | "bin": {
13 | "exm": "dist/cli/index.js",
14 | "exm-cli": "dist/cli/index.js"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/exmbuild/exm-js-sdk.git"
19 | },
20 | "author": "",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/exmbuild/exm-js-sdk/issues"
24 | },
25 | "homepage": "https://github.com/exmbuild/exm-js-sdk#readme",
26 | "dependencies": {
27 | "@types/node": "^18.0.0",
28 | "chalk": "^4.1.2",
29 | "cli-box": "^6.0.10",
30 | "commander": "^9.3.0",
31 | "dotenv": "^16.0.1",
32 | "find-cache-dir": "^3.3.2",
33 | "isomorphic-fetch": "^3.0.0",
34 | "prompt-confirm": "^2.0.4",
35 | "three-em-0-3-09": "npm:@three-em/node@^0.3.09",
36 | "three-em-0-3-12": "npm:@three-em/node@^0.3.12",
37 | "three-em-0-3-13": "npm:@three-em/node@^0.3.13",
38 | "three-em-0-3-14": "npm:@three-em/node@^0.3.14",
39 | "three-em-0-3-15": "npm:@three-em/node@^0.3.15",
40 | "three-em-0-3-16": "npm:@three-em/node@^0.3.16",
41 | "three-em-0-3-17": "npm:@three-em/node@^0.3.17",
42 | "three-em-0-3-20": "npm:@three-em/node@^0.3.20",
43 | "three-em-0-3-21": "npm:@three-em/node@^0.3.21",
44 | "three-em-0-3-22": "npm:@three-em/node@^0.3.22"
45 | },
46 | "devDependencies": {
47 | "@types/jest": "^29.1.0",
48 | "jest": "^29.1.1",
49 | "ts-jest": "^29.0.3"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/cli/cmd/em-wrapper.ts:
--------------------------------------------------------------------------------
1 | import {Exm} from "../../common/em";
2 |
3 | export const getEmToken = () => process.env.EXM_TOKEN || process.env.TOKEN;
4 |
5 | export const em = new Exm({
6 | token: getEmToken()
7 | });
--------------------------------------------------------------------------------
/src/cli/cmd/functions/deploy.ts:
--------------------------------------------------------------------------------
1 | import {readFileSync} from "fs";
2 | import {DeployOpts} from "./model";
3 | import {ContractType, Tag} from "../../../common/model";
4 | import {em} from "../em-wrapper";
5 | import {error} from "../../utils/common";
6 | import {figureOutContractType} from "../../../common/utils/commons";
7 |
8 |
9 | const Confirm = require('prompt-confirm');
10 | const chalk = require('chalk');
11 |
12 | const boxMarks = {
13 | nw: '╭',
14 | n: '─',
15 | ne: '╮',
16 | e: '│',
17 | se: '╯',
18 | s: '─',
19 | sw: '╰',
20 | w: '│'
21 | };
22 |
23 | export const functionDeployCmd = async (opts: DeployOpts) => {
24 | let contentType: ContractType = figureOutContractType(opts.type || opts.src, opts.type !== undefined);
25 | let initState: string;
26 |
27 | if (opts.initState) {
28 | initState = opts.initState;
29 | } else if (opts.initStateSrc) {
30 | initState = readFileSync(opts.initStateSrc, 'utf8');
31 | } else {
32 | error(`Deployment requires --init-state or --init-state-src to be passed`);
33 | }
34 |
35 | let contractData: Uint8Array;
36 |
37 | if(!opts.token) {
38 | error('EXM Token (--token) is required to deploy functions through EXM');
39 | }
40 |
41 | if(opts.contractTx) {
42 | const fetchContractSourceTx = await fetch(`https://arweave.net/${opts.contractTx}`);
43 | if(fetchContractSourceTx.ok) {
44 | contractData = new Uint8Array(await fetchContractSourceTx.arrayBuffer());
45 | } else {
46 | console.error(`Source function transaction ${opts.contractTx} is invalid or does not exist.`);
47 | }
48 | } else if(opts.src) {
49 | contractData = await readFileSync(opts.src);
50 | } else {
51 | error(`Deployment requires --contract-tx or --src`);
52 | }
53 |
54 | const confirmation = await new Confirm(`Do you want to deploy this function?`).run();
55 | if(confirmation) {
56 |
57 | if(opts.token) {
58 | em.changeToken(opts.token);
59 | }
60 |
61 | const Box = require('cli-box');
62 | const beginDeployment = await em.functions.deploy(contractData, initState, contentType);
63 |
64 | const resultBox = new Box({
65 | w: 100,
66 | h: 4,
67 | stringify: false,
68 | marks: boxMarks,
69 | hAlign: 'left',
70 | vAlign: 'top'
71 | }, `Function deployed 🎉
72 | ▸ EXM Function ID : ${beginDeployment.id}
73 | ▸ EXM Function Source : https://arweave.net/${beginDeployment.id}
74 | ▸ Function URL : https://${beginDeployment.id}.exm.run`);
75 |
76 | console.log(resultBox.stringify());
77 |
78 | const functionUrlExample = new Box({
79 | w: 100,
80 | h: 8,
81 | stringify: false,
82 | marks: boxMarks,
83 | hAlign: 'left',
84 | vAlign: 'top'
85 | }, `${chalk.blue('Id: ')} ${beginDeployment.id}
86 |
87 | curl --location --request POST 'https://${beginDeployment.id}.exm.run'
88 | --header 'Content-Type: application/json'
89 | --data-raw '{}' ${chalk.blue('<= Any JSON body')}
90 |
91 | Documentation: https://docs.exm.dev/trustless-serverless-functions/function-urls
92 | `);
93 |
94 | console.log(functionUrlExample.stringify());
95 | }
96 | }
--------------------------------------------------------------------------------
/src/cli/cmd/functions/evaluate/app-simulator.ts:
--------------------------------------------------------------------------------
1 | import {simulateContract, SimulateExecutionContext} from "three-em-0-3-16";
2 |
3 | export interface ExmRunContext extends SimulateExecutionContext {
4 | version: string;
5 | [k: string]: any
6 | }
7 |
8 | export const runExmFunction = (context: ExmRunContext) => {
9 | const isVersion = (executionVersion: string) => (executorVersion: string) => {
10 | return (executionVersion.includes(`^${executorVersion}`) || executionVersion.includes(executorVersion))
11 | }
12 |
13 | if(isVersion(context.version)('0.3.12')) {
14 | const simulateContract = require('three-em-0-3-12').simulateContract;
15 | return simulateContract(context.contractId, context.interactions, context.contractInitState, context.maybeConfig, context.maybeCache, context.maybeBundledContract, context.maybeSettings, context.maybeExmContext);
16 | } else if(isVersion(context.version)('0.3.13')) {
17 | const simulateContract = require('three-em-0-3-13').simulateContract;
18 | return simulateContract(context);
19 | } else if(isVersion(context.version)('0.3.14')) {
20 | const simulateContract = require('three-em-0-3-14').simulateContract;
21 | return simulateContract(context);
22 | } else if(isVersion(context.version)('0.3.15')) {
23 | const simulateContract = require('three-em-0-3-15').simulateContract;
24 | return simulateContract(context);
25 | } else if(isVersion(context.version)('0.3.16')) {
26 | const simulateContract = require('three-em-0-3-16').simulateContract;
27 | return simulateContract(context);
28 | } else if(isVersion(context.version)('0.3.17')) {
29 | const simulateContract = require('three-em-0-3-16').simulateContract;
30 | return simulateContract(context);
31 | } else if(isVersion(context.version)('0.3.20')) {
32 | const simulateContract = require('three-em-0-3-16').simulateContract;
33 | return simulateContract(context);
34 | } else {
35 | const simulateContract = require('three-em-0-3-09').simulateContract;
36 | return simulateContract(context.contractId, context.interactions, context.contractInitState, context.maybeConfig, context.maybeCache, context.maybeBundledContract, context.maybeSettings, context.maybeExmContext);
37 | }
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/src/cli/cmd/functions/evaluate/arweave-evaluate.ts:
--------------------------------------------------------------------------------
1 | import {ArweaveReader, Paginator} from "./readers/arweave/arweave-reader";
2 | import {buildContext, CommonEvaluateOpts} from "./common";
3 | import {cacheBundle} from "./fetch-bundle";
4 | import {runExmFunction} from "./app-simulator";
5 |
6 | export const arweaveEvaluate = async (opts: CommonEvaluateOpts) => {
7 | const arweaveReader = new ArweaveReader();
8 |
9 | let fetchBundles = true;
10 | let cursor = undefined;
11 | let state = undefined;
12 |
13 | while (fetchBundles) {
14 | let paginator: Paginator = { exmFunctionId: opts.exmFunctionId };
15 |
16 | if(cursor) {
17 | paginator.after = cursor;
18 | }
19 |
20 | const bundles = await arweaveReader.fetchBundles(paginator);
21 | if(bundles.length > 0) {
22 | const lastBundle = bundles[bundles.length - 1];
23 | cursor = lastBundle.after;
24 | }
25 |
26 | for (const bundleElement of bundles) {
27 | let bundleData = await cacheBundle(bundleElement.id, opts.cache);
28 | const run = await runExmFunction({
29 | version: bundleElement.threeEmExecutorVersion,
30 | contractId: opts.exmFunctionId,
31 | interactions: bundleData.entities.map((i) => i.raw),
32 | contractInitState: JSON.stringify(state?.state),
33 | maybeConfig: undefined,
34 | maybeCache: false,
35 | maybeBundledContract: bundleElement.isExmFunctionExmDeployed,
36 | maybeSettings: {
37 | 'LAZY_EVALUATION': true
38 | },
39 | maybeExmContext: buildContext(bundleData.exmContext)
40 | });
41 | state = run;
42 | }
43 |
44 | if(bundles.length <= 0) {
45 | fetchBundles = false;
46 | }
47 | }
48 |
49 | return state;
50 | }
--------------------------------------------------------------------------------
/src/cli/cmd/functions/evaluate/common.ts:
--------------------------------------------------------------------------------
1 | import {Tag} from "../../../../common/model";
2 |
3 | export interface ExmContext {
4 | requests: Record
5 | }
6 |
7 | export interface BundleTxEntity {
8 | raw: {
9 | id: string,
10 | owner: string,
11 | quantity: string,
12 | reward: string,
13 | tags: Tag[],
14 | input: any
15 | },
16 | metadata: {
17 | createdBy: string,
18 | createdWhen: number,
19 | executorVersion: string,
20 | internalId: number;
21 | }
22 | }
23 |
24 | export interface BundleBody {
25 | entities: BundleTxEntity[],
26 | exmContext: ExmContext
27 | }
28 |
29 | export interface CommonEvaluateOpts {
30 | exmFunctionId: string
31 | cache?: boolean
32 | }
33 |
34 | export const buildContext = (exmContext: ExmContext) => {
35 | if(exmContext) {
36 | if(!exmContext.requests) {
37 | exmContext.requests = {};
38 | }
39 | return exmContext;
40 | } else {
41 | return {
42 | requests: {}
43 | }
44 | }
45 |
46 | }
--------------------------------------------------------------------------------
/src/cli/cmd/functions/evaluate/evaluate.ts:
--------------------------------------------------------------------------------
1 | import {arweaveEvaluate} from "./arweave-evaluate";
2 |
3 | export const evaluateExmFunction = async (txId: string) => {
4 | const readState = JSON.stringify((await arweaveEvaluate({
5 | exmFunctionId: txId,
6 | cache: true,
7 | })).state, null, 2);
8 | console.log(readState);
9 | }
--------------------------------------------------------------------------------
/src/cli/cmd/functions/evaluate/fetch-bundle.ts:
--------------------------------------------------------------------------------
1 | import findCacheDir from "find-cache-dir";
2 | import { writeFile } from 'fs';
3 | import path from 'path';
4 | import {BundleBody} from "./common";
5 |
6 | let __cacheDir = undefined;
7 |
8 | export const cacheBundle = async (bundleId: string, cache?: boolean): Promise => {
9 | if(cache) {
10 | __cacheDir = findCacheDir({
11 | name: 'exm',
12 | create: true
13 | });
14 | }
15 |
16 | const fetchBundle = await fetch(`https://arweave.net/${bundleId}`);
17 | if(fetchBundle.ok) {
18 | const bundleData = await fetchBundle.json();
19 | if (cache) {
20 | if(__cacheDir) {
21 | writeFile(path.join(__cacheDir, `${bundleId}.json`), JSON.stringify(bundleData), () => {
22 | });
23 | } else {
24 | console.log(`Cache Dir not found. Skipping ⏭️\n`);
25 | }
26 | }
27 | return bundleData;
28 | }
29 |
30 | throw new Error(`Bundle ${bundleId} could not be fetched`);
31 | }
--------------------------------------------------------------------------------
/src/cli/cmd/functions/evaluate/readers/arweave/arweave-reader.ts:
--------------------------------------------------------------------------------
1 | import {BundleQueryResponse, ReaderInterface} from "../reader.interface";
2 | import {BundleBody} from "../../common";
3 | import {postRequest} from "../../../../../../common/utils/commons";
4 | import GQLResultInterface from "./graphql";
5 |
6 | export interface Paginator {
7 | exmFunctionId: string;
8 | after?: string;
9 | }
10 |
11 | export class ArweaveReader implements ReaderInterface {
12 |
13 | async fetchBundles(paginator: Paginator): Promise[]> {
14 | const req = await postRequest(`https://arweave.net/graphql`, this.buildGqlQuery(paginator.exmFunctionId, paginator.after));
15 | const jsonResp: GQLResultInterface = await req.json();
16 | return jsonResp.data.transactions.edges.map((i) => ({
17 | id: i.node.id,
18 | pseudoTimestamp: Number(i.node.tags.find((tag) => tag.name === 'Pseudo-Timestamp')!.value),
19 | functionId: (i.node?.tags || []).find((tag) => tag.name === 'Function')!.value,
20 | blockHeight: i.node?.block?.height || 0,
21 | threeEmExecutorVersion: (i.node?.tags || []).find((tag) => tag.name === '3EM-Executor-Version')?.value!,
22 | after: i.cursor,
23 | isExmFunctionExmDeployed: (i.node?.tags || []).find((tag) => tag.name === '3EM-Function-Deployed')?.value === 'true' || true
24 | }));
25 | }
26 |
27 | private buildGqlQuery(exmFunctionId: string, after?: string) {
28 | const query = `# Write your query or mutation here
29 | query {
30 | transactions(
31 | tags: [{ name: "App-Name", values: ["EM"] }, { name: "Type", values: ["Serverless"] }, { name: "Function", values: ["${exmFunctionId}"] }]
32 | owners: ["kljUVhFCOmFqIow8veT8WYFu65RrOcfzL3m672eatao"]
33 | sort: HEIGHT_ASC
34 | first: 100
35 | ${after ? `after: "${after}"` : ''}
36 | ) {
37 | edges {
38 | node {
39 | id
40 | owner {
41 | address
42 | }
43 | block {
44 | height
45 | }
46 | tags {
47 | name
48 | value
49 | }
50 | }
51 | cursor
52 | }
53 | pageInfo {
54 | hasNextPage
55 | }
56 | }
57 | }`;
58 | return {
59 | query,
60 | variables: {}
61 | }
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/src/cli/cmd/functions/evaluate/readers/arweave/graphql.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | export interface GQLPageInfoInterface {
4 | hasNextPage: boolean;
5 | }
6 |
7 | export interface GQLOwnerInterface {
8 | address: string;
9 | key: string;
10 | }
11 |
12 | export interface GQLAmountInterface {
13 | winston: string;
14 | ar: string;
15 | }
16 |
17 | export interface GQLMetaDataInterface {
18 | size: number;
19 | type: string;
20 | }
21 |
22 | export interface GQLTagInterface {
23 | name: string;
24 | value: string;
25 | }
26 |
27 | export interface GQLBlockInterface {
28 | id: string;
29 | timestamp: number;
30 | height: number;
31 | previous: string;
32 | }
33 |
34 | export interface GQLNodeInterface {
35 | id: string;
36 | anchor: string;
37 | signature: string;
38 | recipient: string;
39 | owner: GQLOwnerInterface;
40 | fee: GQLAmountInterface;
41 | quantity: GQLAmountInterface;
42 | data: GQLMetaDataInterface;
43 | tags: GQLTagInterface[];
44 | block: GQLBlockInterface;
45 | parent: {
46 | id: string;
47 | };
48 | bundledIn: {
49 | id: string;
50 | };
51 | }
52 |
53 | export interface GQLEdgeInterface {
54 | cursor: string;
55 | node: GQLNodeInterface;
56 | }
57 |
58 | export interface GQLTransactionsResultInterface {
59 | pageInfo: GQLPageInfoInterface;
60 | edges: GQLEdgeInterface[];
61 | }
62 |
63 | export default interface GQLResultInterface {
64 | data: {
65 | transactions: GQLTransactionsResultInterface;
66 | };
67 | }
--------------------------------------------------------------------------------
/src/cli/cmd/functions/evaluate/readers/reader.interface.ts:
--------------------------------------------------------------------------------
1 | import { BundleBody } from "../common";
2 |
3 | export interface BundleQueryResponse {
4 | id: string;
5 | pseudoTimestamp: number;
6 | functionId: string;
7 | blockHeight: number;
8 | threeEmExecutorVersion: string;
9 | isExmFunctionExmDeployed: boolean;
10 | after?: After
11 | }
12 |
13 | export interface ReaderInterface {
14 | fetchBundles: (paginator: Paginator) => Promise[]>
15 | }
--------------------------------------------------------------------------------
/src/cli/cmd/functions/function-test.ts:
--------------------------------------------------------------------------------
1 | import {FunctionTestOpts} from "./model";
2 | import {error} from "../../utils/common";
3 | import {readFileSync} from "fs";
4 | import {figureOutContractType} from "../../../common/utils/commons";
5 | import {createWrite, TestFunction} from "../../../test/test-function";
6 | import {ContractType} from "../../../common/model";
7 | import {SimulateContractType} from "three-em-0-3-21";
8 |
9 | export const functionTest = async (opts: FunctionTestOpts) => {
10 | if(!opts.src) {
11 | error("A function source path must be specified");
12 | return;
13 | }
14 |
15 | let initState = undefined;
16 |
17 | if(opts.initStateSrc) {
18 | initState = readFileSync(opts.initStateSrc, 'utf8')
19 | } else {
20 | if (!opts.initState) {
21 | error("Initial state must be specified and it must be a JSON-valid expression");
22 | return;
23 | }
24 | initState = opts.initState;
25 | }
26 |
27 | if(!opts.input || opts.input.length <= 0) {
28 | error("Single or multiple inputs must be specified");
29 | return;
30 | }
31 |
32 | const bufferSource = readFileSync(opts.src);
33 | const contractType = figureOutContractType(opts.type || opts.src, opts.type !== undefined);
34 |
35 | const writeOperations = opts.input.map((input) => createWrite(input, [], undefined, true));
36 | const testFunction = await TestFunction({
37 | functionType: contractType == ContractType.WASM ? SimulateContractType.WASM : SimulateContractType.JAVASCRIPT,
38 | functionInitState: JSON.parse(initState),
39 | functionSource: bufferSource,
40 | writes: writeOperations
41 | });
42 |
43 | console.log(JSON.stringify({
44 | state: testFunction.state,
45 | validity: testFunction.validity
46 | }, null, 2));
47 | return testFunction;
48 | }
--------------------------------------------------------------------------------
/src/cli/cmd/functions/model.ts:
--------------------------------------------------------------------------------
1 | import {ContractType} from "../../../common/model";
2 |
3 | export interface DeployOpts {
4 | useArweave?: boolean;
5 | wallet?: string;
6 | src?: string;
7 | contractTx?: string;
8 | initState?: string;
9 | initStateSrc?: string;
10 | token?: string;
11 | type?: "wasm" | "js" | "evm" | ContractType;
12 | }
13 |
14 | export interface FunctionTestOpts {
15 | src?: string;
16 | input: Array
17 | initState?: string
18 | initStateSrc?: string;
19 | type?: string;
20 | }
--------------------------------------------------------------------------------
/src/cli/cmd/functions/read.ts:
--------------------------------------------------------------------------------
1 | import {em} from "../em-wrapper";
2 |
3 | export const functionReadCmd = async (txId: string) => {
4 | await em.functions.read(txId).then((r) => {
5 | console.log(JSON.stringify(r, null, 2));
6 | });
7 | }
--------------------------------------------------------------------------------
/src/cli/cmd/functions/write.ts:
--------------------------------------------------------------------------------
1 | import { parse } from "dotenv";
2 | import {Tag} from "../../../common/model";
3 | import {em} from "../em-wrapper";
4 | import {WriteOpFailure} from "../../../common/exceptions/writeOpFailure";
5 |
6 | export const functionWriteCmd = async (txId: string, opts: any) => {
7 | const tagsObj = parse(opts.tags.join('\n'));
8 | const tagsArray: Tag[] = Object.keys(tagsObj).map((i) => ({
9 | name: i,
10 | value: tagsObj[i]
11 | }));
12 |
13 | let input: any;
14 |
15 | try {
16 | const parsedInput = JSON.parse(opts.input);
17 | input = JSON.stringify(parsedInput);
18 | } catch(e) {
19 | input = opts.input;
20 | }
21 |
22 | if(opts.token) {
23 | em.changeToken(opts.token);
24 | }
25 |
26 | await em.functions.writeRaw(txId, [
27 | {
28 | input,
29 | tags: tagsArray
30 | }
31 | ]).then(({ data: { pseudoId, execution: { state, validity } } }) => {
32 | if (validity[pseudoId]) {
33 | console.log(
34 | `Write query ${pseudoId} (pseudo-id) was successfully executed`,
35 | );
36 | } else {
37 | console.log(
38 | `Write query ${pseudoId} (pseudo-id) could not be executed`,
39 | );
40 | }
41 | if(opts.showOutput) {
42 | console.log(`\n${JSON.stringify({ state, validity}, null, 2)}`)
43 | }
44 | }).catch((e) => {
45 | if(e instanceof WriteOpFailure) {
46 | if(e.statusCode === 403) {
47 | console.log('403: Write operation could not be completed because the resource could not be accessed or token is invalid.');
48 | } else {
49 | console.error(e.message);
50 | }
51 | } else {
52 | console.error(e.message);
53 | }
54 | process.exit(1);
55 | });
56 | }
--------------------------------------------------------------------------------
/src/cli/index.ts:
--------------------------------------------------------------------------------
1 | import { Command } from "commander";
2 | import {functionReadCmd} from "./cmd/functions/read";
3 | import {functionWriteCmd} from "./cmd/functions/write";
4 | import {functionDeployCmd} from "./cmd/functions/deploy";
5 | import {evaluateExmFunction} from "./cmd/functions/evaluate/evaluate";
6 | import {functionTest} from "./cmd/functions/function-test";
7 |
8 | const program = new Command();
9 |
10 | program.name('exm')
11 | .description('A CLI to interact with the Execution Machine.')
12 | .version('0.1.5');
13 |
14 | program.command('function:read')
15 | .alias('fx:r')
16 | .description('Read the state of a function in EXM.')
17 | .argument('', 'Arweave ID of function.')
18 | .action(functionReadCmd);
19 |
20 | program.command('function:write')
21 | .alias('fx:w')
22 | .description('Read the state of a function in EXM.')
23 | .argument('', 'Arweave ID of function.')
24 | .requiredOption('-i, --input ', 'Input to be passed to the function')
25 | .option('-t, --tags ', 'Tags to be used during write operation evaluation. Usage: --tags tag1=value1 --tags tag2="value 2"', (value: string, previous: string[]) => previous.concat([value]), [])
26 | .option('-t, --token ', 'Execution Machine API Token to be used.')
27 | .option('--show-output', 'Show optimistic execution output from write operation')
28 | .action(functionWriteCmd);
29 |
30 | program.command('function:deploy')
31 | .alias('fx:d')
32 | .description('Deploy a EXM compatible function to Arweave.')
33 | .option('-s, --src ', 'Path to source code of function. Example: /Documents/function.wasm .')
34 | .option('--contract-tx ', 'ID of Source Function already deployed to Arweave.')
35 | .option('-i, --init-state ', 'Init State for Function to be deployed under init function.')
36 | .option('--init-state-src ', 'Path to init state file.')
37 | .option('-t, --type ', 'Type of function to be deployed.')
38 | .option('-t, --token ', 'Execution Machine API Token to be used.')
39 | .action(functionDeployCmd);
40 |
41 | program.command('function:evaluate')
42 | .alias('fx:e')
43 | .description('Evaluates the state of an EXM application')
44 | .argument('', 'ID of deployed function')
45 | .action(evaluateExmFunction);
46 |
47 | program.command('function:test')
48 | .alias('fx:t')
49 | .description('Test a function inside a simulated EXM environment')
50 | .option('-s, --src ', 'Path to source code of function. Example: /Documents/function.js')
51 | .option('--init-state ', 'JSON value containing the initial state of the function')
52 | .option('--init-state-src ', 'Path to init state file.')
53 | .option('-t, --type ', 'Type of function to be deployed.')
54 | .option('-i, --input ', `Inputs to be used during evaluation. Usage: --input '{\"someProperty\":\"value\"} --input '{}'`, (value: string, previous: string[]) => previous.concat([value]), [])
55 | // @ts-ignore
56 | .action(functionTest);
57 |
58 | program.parse(process.argv);
59 |
--------------------------------------------------------------------------------
/src/cli/utils/common.ts:
--------------------------------------------------------------------------------
1 | export const error = (message: string) => {
2 | console.error(message);
3 | process.exit(1);
4 | }
--------------------------------------------------------------------------------
/src/common/em.ts:
--------------------------------------------------------------------------------
1 | import {ContractType, DeployOpts, EmOpts, WriteAction} from "./model";
2 | import {writeFunction} from "./functions/write";
3 | import {readFunction} from "./functions/read";
4 | import {deployFunction} from "./functions/deploy";
5 |
6 | export class Exm {
7 |
8 | constructor(private readonly opts: EmOpts) {
9 | if(global && !global.fetch) {
10 | require('isomorphic-fetch');
11 | }
12 | }
13 |
14 | /**
15 | * Changes the token passed in constructor
16 | * @param token
17 | */
18 | changeToken(token: string) {
19 | this.opts.token = token;
20 | }
21 |
22 | /**
23 | * Gets the methods related to EXM Functions product
24 | */
25 | get functions() {
26 | return {
27 | writeRaw: async (functionId: string, writeOps: Array | WriteAction, ignoreState?: boolean) => writeFunction(functionId, writeOps, this.opts.token, ignoreState, true),
28 | write: async (functionId: string, inputs: any | Array, ignoreState?: boolean) => writeFunction(functionId, inputs, this.opts.token, ignoreState, false),
29 | read: async (functionId: string) => readFunction(functionId),
30 | deploy: async(contractSrc: Uint8Array, contractInitState: any, contractType: ContractType, opts?: DeployOpts) => deployFunction(contractSrc, contractInitState, contractType, this.opts.token, opts)
31 | }
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/common/exceptions/deployOpFailure.ts:
--------------------------------------------------------------------------------
1 | export class DeployOpFailure extends Error {
2 | constructor(message: string, public statusCode?: number) {
3 | super(message);
4 | }
5 | }
--------------------------------------------------------------------------------
/src/common/exceptions/readFailure.ts:
--------------------------------------------------------------------------------
1 | export class ReadFailure extends Error {
2 | constructor(message?: string) {
3 | super(message);
4 | }
5 | }
--------------------------------------------------------------------------------
/src/common/exceptions/writeOpFailure.ts:
--------------------------------------------------------------------------------
1 | export class WriteOpFailure extends Error {
2 | constructor(message: string, public statusCode?: number) {
3 | super(message);
4 | }
5 | }
--------------------------------------------------------------------------------
/src/common/functions/deploy.ts:
--------------------------------------------------------------------------------
1 | import {ContractType, DeployOpBody, DeployOpResult, DeployOpts} from "../model";
2 | import {postRequest} from "../utils/commons";
3 | import {EmVars} from "../vars";
4 | import {DeployOpFailure} from "../exceptions/deployOpFailure";
5 |
6 | /**
7 | * This method allows you to deploy a function application through EXM.
8 | * No AR or Wallets are needed to execute the deployment.
9 | *
10 | * @param contractSrc Bytes (UInt8Array) of source code of function to be deployed
11 | * @param contractInitState String representing the initial state of the contract.
12 | * @param contractType JS, EVM, WASM.
13 | * @param emToken EM Token to authenticate request
14 | * @param opts Options for deployment
15 | */
16 | export const deployFunction = async(contractSrc: Uint8Array, contractInitState: any, contractType: ContractType, emToken: string, opts?: DeployOpts) => {
17 | try {
18 | const bytes = Array.from(contractSrc.values());
19 | const body: Partial = {
20 | contractSrc: bytes
21 | };
22 | body.contentType = contractType || ContractType.JS;
23 | let initState = '{}';
24 |
25 | if(contractInitState) {
26 | if(typeof contractInitState === 'object') {
27 | initState = JSON.stringify(contractInitState);
28 | } else {
29 | initState = String(contractInitState);
30 | }
31 | }
32 |
33 | body.initState = initState;
34 | body.contractOwner = (opts || {}).ownerAddress || "";
35 |
36 | const data = await postRequest(`${EmVars.EM_BACKEND_URL}/contract/deploy?token=${emToken}`, body);
37 |
38 | if (data.ok) {
39 | const json = await data.json();
40 | return json as DeployOpResult;
41 | } else {
42 | throw new DeployOpFailure(`${data.status}: Function application could not be deployed.`, data.status);
43 | }
44 | } catch(e) {
45 | if(e instanceof DeployOpFailure) {
46 | throw e;
47 | } else {
48 | throw new DeployOpFailure(`${e.toString()}`);
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/src/common/functions/read.ts:
--------------------------------------------------------------------------------
1 | import {ReadResult} from "../model";
2 | import {EmVars} from "../vars";
3 | import {ReadFailure} from "../exceptions/readFailure";
4 |
5 | /**
6 | * This method allows you to read the state of a function being used in EM (Execution Machine)
7 | * No token is needed since states are technically public from an evaluation perspective
8 | *
9 | * @param functionId The function ID (An Arweave valid TX id) to obtain the state for
10 | */
11 | export const readFunction = async (functionId: string): Promise> => {
12 | const fetchState = await fetch(`${EmVars.EM_READ_URL}/${functionId}`);
13 | if(fetchState.ok) {
14 | return (await fetchState.json()) as ReadResult;
15 | } else {
16 | throw new ReadFailure(`EM was not able to read state for function ${functionId}`);
17 | }
18 | }
--------------------------------------------------------------------------------
/src/common/functions/write.ts:
--------------------------------------------------------------------------------
1 | import {EmVars} from "../vars";
2 | import {postRequest} from "../utils/commons";
3 | import {WriteAction, WriteOpBody, WriteOpResult} from "../model";
4 | import {WriteOpFailure} from "../exceptions/writeOpFailure";
5 |
6 | const inputToString = (input: any) => {
7 | if(typeof input !== 'string') {
8 | input = JSON.stringify(input);
9 | }
10 |
11 | return input;
12 | }
13 |
14 | const cleanInput = (writeAction: WriteAction) => {
15 | if(!writeAction.input) {
16 | throw new Error(`Property 'input' is required in a write operation`);
17 | }
18 | if(!writeAction.tags) {
19 | writeAction.tags = [];
20 | }
21 |
22 | writeAction.input = inputToString(writeAction.input);
23 |
24 | return writeAction;
25 | }
26 |
27 | /**
28 | * This method allows you to execute write operations inside EM Trustless, Serverless Functions.
29 | * By requesting to the module `em-backend`, it creates a write operation which will be automatically processed.
30 | * Note that the result returned by this function is optimistic as the final processing for the global state of the function
31 | * Might take a few more seconds in order for it to be available and propagated.
32 | *
33 | *
34 | * @param functionId The function ID (An Arweave valid TX id) to be processed
35 | * @param writeOps A single operation or an array of operations to be sent. Note a limit of 499 write operations are enforced.
36 | * @param emToken EM Token to authenticate request
37 | * @param ignoreState Whether optimistic state should not be returned
38 | * @param raw Whether it's a write input or an input to be constructed.
39 | */
40 | export const writeFunction = async (functionId: string, writeOps: WriteAction[] | any, emToken: string, ignoreState: boolean | undefined, raw: boolean): Promise> => {
41 | let body: Partial = {
42 | functionId
43 | };
44 |
45 | let inputs: WriteAction[] = [];
46 |
47 | if(raw) {
48 | let writeOpsScope: WriteAction | WriteAction[] = writeOps;
49 | if(Array.isArray(writeOpsScope)) {
50 | writeOpsScope.forEach((i, index) => {
51 | writeOpsScope[index] = cleanInput(i);
52 | });
53 | } else {
54 | writeOpsScope = cleanInput(writeOpsScope);
55 | }
56 |
57 | inputs = Array.isArray(writeOpsScope) ? writeOpsScope : [writeOpsScope];
58 | } else {
59 | if(Array.isArray(writeOps)) {
60 | inputs = writeOps.map((i) => ({ input: inputToString(i), tags: [] }));
61 | } else {
62 | inputs = [{ input: inputToString(writeOps), tags: [] }]
63 | }
64 | }
65 |
66 | body.inputs = inputs;
67 |
68 | if(body.inputs.length > 499) {
69 | throw new WriteOpFailure("Only 499 writes are allowed in a single query.");
70 | }
71 |
72 | let reqUrl = `${EmVars.EM_BACKEND_URL}/transactions?token=${emToken}`;
73 | if(ignoreState) {
74 | reqUrl = `${reqUrl}&ignoreState=true`;
75 | }
76 |
77 | const data = await postRequest(reqUrl, body);
78 | let bodyJson: any = await data.json();
79 |
80 | if(data.ok) {
81 | return bodyJson as WriteOpResult
82 | } else {
83 | if(bodyJson && bodyJson.statusCode && bodyJson.error && bodyJson.message) {
84 | throw new WriteOpFailure(`${bodyJson.statusCode}: Internal server error while sending write operation to function ${functionId}. "Error: ${bodyJson.message}"`, bodyJson.statusCode);
85 | } else {
86 | throw new WriteOpFailure(`${data.status}: Something went wrong, write operation for function ${functionId} could not be sent.`);
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/src/common/model.ts:
--------------------------------------------------------------------------------
1 | export enum ContractType {
2 | JS = "application/javascript", // 'application/javascript', 'application/typescript'
3 | EVM = "application/octet-stream", // 'application/vnd.exm.evm', 'application/vnd.exm.sol'
4 | WASM = "application/wasm", // 'application/vnd.exm.wasm+rust', 'application/vnd.exm.wasm+c', 'application/vnd.exm.wasm+cpp'
5 | }
6 |
7 | export interface EmOpts {
8 | token: string;
9 | }
10 |
11 | export interface Tag {
12 | name: string;
13 | value: string;
14 | }
15 |
16 | export interface WriteAction {
17 | input: T,
18 | tags: Array
19 | }
20 |
21 | export interface WriteOpBody {
22 | functionId: string;
23 | inputs: Array>
24 | }
25 |
26 | export interface WriteOpResult {
27 | status: "SUCCESS",
28 | data: {
29 | pseudoId: string;
30 | execution: {
31 | state: T,
32 | validity: Record
33 | }
34 | }
35 | }
36 |
37 | export interface ReadResult {
38 | state: T;
39 | }
40 |
41 | export interface DeployOpBody {
42 | contractSrc: Array;
43 | contentType: string;
44 | initState: string;
45 | contractOwner?: string;
46 | }
47 |
48 | export interface DeployOpResult {
49 | id: string;
50 | }
51 |
52 | export interface DeployOpts {
53 | ownerAddress?: string;
54 | }
--------------------------------------------------------------------------------
/src/common/utils/commons.ts:
--------------------------------------------------------------------------------
1 | import {ContractType} from "../model";
2 |
3 | export const postRequest = (url: string, body: any) => {
4 | return fetch(url, {
5 | method: 'POST',
6 | headers: {
7 | 'Accept': 'application/json',
8 | 'Content-Type': 'application/json'
9 | },
10 | body: JSON.stringify(body)
11 | });
12 | }
13 |
14 | export const guidGenerator = () => {
15 | const S4 = function() {
16 | return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
17 | };
18 | return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
19 | }
20 |
21 | export const figureOutContractType = (input: string | undefined, byType: boolean): ContractType => {
22 | let contentType: ContractType = ContractType.JS;
23 | if(input) {
24 | const lowerCaseInput = input.toLowerCase();
25 |
26 | if (byType) {
27 | switch (lowerCaseInput) {
28 | case "wasm":
29 | contentType = ContractType.WASM;
30 | break;
31 | case "evm":
32 | contentType = ContractType.EVM;
33 | break;
34 | case "js":
35 | contentType = ContractType.JS;
36 | break;
37 | }
38 | } else {
39 | if (lowerCaseInput.endsWith(".js")) {
40 | contentType = ContractType.JS;
41 | } else if (lowerCaseInput.endsWith(".wasm")) {
42 | contentType = ContractType.WASM;
43 | } else {
44 | contentType = ContractType.JS;
45 | }
46 | }
47 | }
48 |
49 | return contentType;
50 | }
--------------------------------------------------------------------------------
/src/common/vars.ts:
--------------------------------------------------------------------------------
1 | export class EmVars {
2 | public static EM_BACKEND_URL: string = 'https://api.exm.dev/api';
3 | public static EM_READ_URL: string = 'https://api.exm.dev/read';
4 | }
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './common/em';
2 | export * from './common/model';
3 | export * from './common/functions/write';
4 | export * from './common/functions/read';
5 | export * from './common/functions/deploy';
6 | export * from './common/exceptions/readFailure';
7 | export * from './common/exceptions/writeOpFailure';
8 | export * from './test/test-function';
--------------------------------------------------------------------------------
/src/test/test-function.ts:
--------------------------------------------------------------------------------
1 | import {ExecuteConfig, simulateContract, SimulateContractType as ContractType, SimulateInput, Tag} from "three-em-0-3-22";
2 | import {guidGenerator} from "../common/utils/commons";
3 |
4 | // @ts-ignore
5 | export const FunctionType = Object.assign({}, ContractType);
6 | export type FunctionType = ContractType;
7 |
8 | export type WriteInput = SimulateInput;
9 | export type ExecutionConfig = ExecuteConfig;
10 |
11 | export interface TestFunctionOpts {
12 | functionSource: Uint8Array,
13 | functionType: ContractType,
14 | functionInitState: any;
15 | writes: WriteInput[];
16 | gatewayConfig?: ExecutionConfig
17 | settings?: Record | undefined | null,
18 | exmContext?: any | undefined | null
19 | }
20 |
21 | export const createWrite = (input: any, tags?: Array, opts?: Partial>, inputIsJson?: boolean): SimulateInput => {
22 | return {
23 | id: opts?.id || guidGenerator(),
24 | owner: opts?.owner || "@test",
25 | quantity: opts?.quantity || "0",
26 | reward: opts?.reward || "0",
27 | target: opts?.target,
28 | tags: tags || [],
29 | block: opts?.block,
30 | input: inputIsJson ? input : JSON.stringify(input)
31 | };
32 | }
33 |
34 | export const TestFunction = async (opts: TestFunctionOpts) => {
35 | return await simulateContract({
36 | contractId: "",
37 | maybeContractSource: {
38 | // @ts-ignore
39 | contractSrc: opts.functionSource,
40 | contractType: opts.functionType
41 | },
42 | contractInitState: JSON.stringify(opts.functionInitState),
43 | maybeConfig: opts.gatewayConfig,
44 | maybeSettings: {
45 | ...(opts.settings || {}),
46 | 'TX_DATE': `${new Date().getTime()}`
47 | },
48 | maybeExmContext: opts.exmContext,
49 | interactions: opts.writes,
50 | maybeCache: false
51 | });
52 | }
--------------------------------------------------------------------------------
/tests/cli-function.tests.ts:
--------------------------------------------------------------------------------
1 | import {functionTest} from "../src/cli/cmd/functions/function-test";
2 | import {TestFunction, FunctionType, createWrite} from "../src";
3 | import {readFileSync} from "fs";
4 |
5 | describe('CLI Functions module', () => {
6 |
7 | test('function test successful', async () => {
8 | const testUserRegistry = await functionTest({
9 | input: [`{"username": "Andres"}`, `{"username": "Superman"}`],
10 | src: './tests/testdata/user-registry.js',
11 | initState: '{"users":[]}',
12 | type: "js"
13 | });
14 | expect(testUserRegistry.state).toEqual({
15 | "users": [
16 | {
17 | "username": "Andres"
18 | },
19 | {
20 | "username": "Superman"
21 | }
22 | ]
23 | });
24 | });
25 |
26 | test('function test successful (init-state file)', async () => {
27 | const testUserRegistry = await functionTest({
28 | input: [`{"username": "Andres"}`, `{"username": "Superman"}`],
29 | src: './tests/testdata/user-registry.js',
30 | initStateSrc: './tests/testdata/user-registry-init.json',
31 | type: "js"
32 | });
33 | expect(testUserRegistry.state).toEqual({
34 | "users": [
35 | {
36 | "username": "Deadpool"
37 | },
38 | {
39 | "username": "Andres"
40 | },
41 | {
42 | "username": "Superman"
43 | }
44 | ]
45 | });
46 | });
47 |
48 | test('Function from import', async () => {
49 | const testUserRegistry = TestFunction({
50 | functionSource: readFileSync("./tests/testdata/user-registry.js"),
51 | functionType: FunctionType.JAVASCRIPT,
52 | functionInitState: {
53 | users: []
54 | },
55 | writes: [createWrite({ username: "Andres" })]
56 | })
57 | })
58 |
59 | });
--------------------------------------------------------------------------------
/tests/testdata/user-registry-init.json:
--------------------------------------------------------------------------------
1 | {
2 | "users": [{
3 | "username": "Deadpool"
4 | }]
5 | }
--------------------------------------------------------------------------------
/tests/testdata/user-registry.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @param state is the current state your application holds
4 | * @param action is an object containing { input, caller } . Most of the times you will only use `action.input` which contains the input passed as a write operation
5 | * @returns {Promise<{ users: Array<{ username: string}> }>}
6 | */
7 | export async function handle(state, action) {
8 | const { username } = action.input;
9 | state.users.push({ username });
10 | return { state };
11 | }
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "tests", "dist", "**/*tests.ts"]
4 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "target": "es2020",
9 | "sourceMap": true,
10 | "outDir": "./dist",
11 | "baseUrl": "./",
12 | "incremental": true,
13 | "skipLibCheck": true,
14 | "strictNullChecks": false,
15 | "noImplicitAny": false,
16 | "strictBindCallApply": false,
17 | "forceConsistentCasingInFileNames": false,
18 | "noFallthroughCasesInSwitch": false,
19 | "esModuleInterop": true
20 | },
21 | "exclude": ["node_modules", "tests", "dist", "**/*tests.ts"]
22 | }
--------------------------------------------------------------------------------