Skip to content

Commit

Permalink
Implement parser and typescript code generator (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
osipov-mit authored Jan 19, 2024
1 parent e8e9e5b commit cd0277c
Show file tree
Hide file tree
Showing 38 changed files with 4,805 additions and 1 deletion.
85 changes: 85 additions & 0 deletions .github/workflows/ci-js.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: 'CI-CD sails-js'

on:
pull_request:
types: [opened, synchronize, reopened, labeled]
branches: [master]
push:
branches: [master]
paths:
- js/**
workflow_dispatch:

jobs:
test:
if: github.event_name == 'pull_request'

runs-on: ubuntu-22.04
env:
RUSTUP_HOME: /tmp/rustup_home
steps:
- name: Cancel previous workflow runs
uses: styfle/cancel-workflow-action@0.4.0
with:
access_token: ${{ github.token }}

- name: Checkout
uses: actions/checkout@v3

- name: "Install: NodeJS 18.x"
uses: actions/setup-node@v3
with:
node-version: 18.x

- name: "Install: pkg dependencies"
working-directory: js
run: yarn install

- name: "Build: sails-js"
working-directory: js
run: yarn build

- name: "Test: run"
working-directory: js
run: yarn test

publish-to-npm:
if: github.event_name == 'push'

runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Use node 18.x
uses: actions/setup-node@v3
with:
node-version: 18.x

- name: Check package version
uses: EndBug/version-check@v1
id: check
with:
file-name: js/package.json
file-url: https://unpkg.com/sails-js@latest/package.json
static-checking: localIsNew

- name: Install dependencies
if: steps.check.outputs.changed == 'true'
working-directory: js
run: yarn install

- name: Build sails-js
if: steps.check.outputs.changed == 'true'
working-directory: js
run: yarn build

- name: Publish
if: steps.check.outputs.changed == 'true'
working-directory: js
run: |
export token=$(printenv $(printenv GITHUB_ACTOR))
npm config set //registry.npmjs.org/:_authToken=$token
npm publish
env:
osipov-mit: ${{ secrets.SAILS_JS_NPM_TOKEN }}
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
node_modules/
js/lib/

.DS_Store

.vscode
target/
.binpath
#.vscode
.vscode
#.log
29 changes: 29 additions & 0 deletions js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Installation
The package can be installed as either a global dependency or a package dependency.

```bash
npm install -g sails-js
```
or
```bash
npm install sails-js
```

## Usage

### CLI
- Generate typescript code from the IDL file
```bash
sails-js generate path/to/sails.idl -o path/to/out/dir
```
This command will generate 2 files to the specified directory.

- Parse IDL file and print the result
```bash
sails-js parse-and-print path/to/sails.idl
```

- Parse IDL file and save the result to a json file
```bash
sails-js parse-into-file path/to/sails.idl path/to/out.json
```
2 changes: 2 additions & 0 deletions js/example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
yarn.lock
11 changes: 11 additions & 0 deletions js/example/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "example",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@gear-js/api": "^0.35.2",
"@polkadot/api": "^10.11.1",
"@polkadot/types": "^10.11.1"
}
}
33 changes: 33 additions & 0 deletions js/example/sails.idl
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
type Alias = u32;

type OptionAlias = opt u8;

type ResultAlias = result (u8, string)

type VecAlias = vec u8;

type ThisThatSvcAppTupleStruct = struct {
bool,
};

type ThisThatSvcAppDoThatParam = struct {
p1: u32,
p2: str,
p3: ThisThatSvcAppManyVariants,
};

type ThisThatSvcAppManyVariants = enum {
One,
Two: u32,
Three: opt u32,
Four: struct { a: u32, b: opt u16 },
Five: struct { str, u32 },
Six: struct { u32 },
};

service {
DoThis : (p1: u32, p2: str, p3: struct { opt str, u8 }, p4: ThisThatSvcAppTupleStruct) -> struct { str, u32 };
DoThat : (param: ThisThatSvcAppDoThatParam) -> result (struct { str, u32 }, struct { str });
query This : () -> u32;
query That : () -> result (str, str);
}
87 changes: 87 additions & 0 deletions js/example/src/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { GearApi } from '@gear-js/api';
import { Transaction } from './transaction.js';
import { IKeyringPair } from '@polkadot/types/types';

export type Alias = bigint;

export type OptionAlias = null | number;

export type ResultAlias = { ok: number } | { err: string };

export type VecAlias = Array<number>;

export type ThisThatSvcAppTupleStruct = [boolean];

export interface ThisThatSvcAppDoThatParam {
p1: bigint;
p2: string;
p3: ThisThatSvcAppManyVariants;
}

export type ThisThatSvcAppManyVariants =
| { One: null }
| { Two: bigint }
| { Three: null | bigint }
| { Four: { a: bigint; b: null | number } }
| { Five: [string, bigint] }
| { Six: [bigint] };

export class Service extends Transaction {
constructor(api: GearApi, public programId: `0x${string}`) {
const types: Record<string, any> = {
Alias: "u32",
OptionAlias: "Option<u8>",
ResultAlias: "Result<u8, string>",
VecAlias: "Vec<u8>",
ThisThatSvcAppTupleStruct: "(bool)",
ThisThatSvcAppDoThatParam: {"p1":"u32","p2":"str","p3":"ThisThatSvcAppManyVariants"},
ThisThatSvcAppManyVariants: {"_enum":{"One":null,"Two":"u32","Three":"Option<u32>","Four":{"a":"u32","b":"Option<u16>"},"Five":"(str, u32)","Six":"(u32)"}},
}
super(api, types);
}

public async doThis(p1: bigint, p2: string, p3: [null | string, number], p4: ThisThatSvcAppTupleStruct, account: `0x${string}` | IKeyringPair): Promise<[string, bigint]> {
const payload = [
...this.registry.createType('String', 'DoThis/').toU8a(),
...this.registry.createType('u32', p1).toU8a(),
...this.registry.createType('str', p2).toU8a(),
...this.registry.createType('(Option<str>, u8)', p3).toU8a(),
...this.registry.createType('ThisThatSvcAppTupleStruct', p4).toU8a(),
];
const replyPayloadBytes = await this.submitMsgAndWaitForReply(
this.programId,
payload,
account,
);
const result = this.registry.createType('(str, u32)', replyPayloadBytes);
return result.toJSON() as [string, bigint];
}

public async doThat(param: ThisThatSvcAppDoThatParam, account: `0x${string}` | IKeyringPair): Promise<{ ok: [string, bigint] } | { err: [string] }> {
const payload = [
...this.registry.createType('String', 'DoThat/').toU8a(),
...this.registry.createType('ThisThatSvcAppDoThatParam', param).toU8a(),
];
const replyPayloadBytes = await this.submitMsgAndWaitForReply(
this.programId,
payload,
account,
);
const result = this.registry.createType('Result<(str, u32), (str)>', replyPayloadBytes);
return result.toJSON() as { ok: [string, bigint] } | { err: [string] };
}

public async this(): Promise<bigint> {
const payload = this.registry.createType('String', 'This/').toU8a();
const stateBytes = await this.api.programState.read({ programId: this.programId, payload});
const result = this.registry.createType('u32', stateBytes);
return result.toBigInt() as bigint;
}

public async that(): Promise<{ ok: string } | { err: string }> {
const payload = this.registry.createType('String', 'That/').toU8a();
const stateBytes = await this.api.programState.read({ programId: this.programId, payload});
const result = this.registry.createType('Result<str, str>', stateBytes);
return result.toJSON() as { ok: string } | { err: string };
}
}
98 changes: 98 additions & 0 deletions js/example/src/transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { GearApi, HexString, MessageQueuedData, UserMessageSentData, decodeAddress } from '@gear-js/api';
import { SubmittableExtrinsic } from '@polkadot/api/types';
import { TypeRegistry } from '@polkadot/types';
import { IKeyringPair, ISubmittableResult } from '@polkadot/types/types';
import { Registry } from '@polkadot/types-codec/types/registry';
import { ReplaySubject } from 'rxjs';

export class Transaction {
protected registry: Registry;

constructor(protected api: GearApi, types: Record<string, any>) {
this.registry = new TypeRegistry();

this.registry.setKnownTypes({ types });
this.registry.register(types);
}

private sendTx(
tx: SubmittableExtrinsic<'promise', ISubmittableResult>,
account: IKeyringPair | string,
): Promise<string> {
return new Promise((resolve, reject) =>
tx
.signAndSend(account, ({ events, status }) => {
if (status.isInBlock) {
let msgId: string;

events.forEach(({ event }) => {
const { method, section, data } = event;
if (method === 'MessageQueued' && section === 'Gear') {
msgId = (data as MessageQueuedData).id.toHex();
} else if (section === 'System' && method === 'ExtrinsicSuccess') {
resolve(msgId);
} else if (section === 'System' && method === 'ExtrinsicFailed') {
reject(this.api.getExtrinsicFailedError(event));
}
});
}
})
.catch((error) => {
reject(error.message);
}),
);
}

private async listenToUserMessageSentEvents(from: string, to: string) {
const subject = new ReplaySubject<UserMessageSentData>(5);
const unsub = await this.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data }) => {
if (data.message.source.eq(from) && data.message.destination.eq(to)) {
subject.next(data);
}
});

return { unsub, subject };
}

private async waitForReply(subject: ReplaySubject<UserMessageSentData>, msgId: string): Promise<HexString> {
return new Promise<HexString>((resolve, reject) => {
subject.subscribe(({ message }) => {
if (message.details.isSome) {
if (message.details.unwrap().to.eq(msgId)) {
if (!message.details.unwrap().code.isSuccess) {
reject(message.payload.toString());
} else {
resolve(message.payload.toHex());
}
}
}
});
});
}

protected async submitMsgAndWaitForReply(
programId: HexString,
payload: any,
account: string | IKeyringPair,
): Promise<HexString> {
const addressHex = decodeAddress(typeof account === 'string' ? account : account.address);

const gasLimit = await this.api.program.calculateGas.handle(addressHex, programId, payload, 0, false);

const tx = this.api.message.send({
destination: programId,
payload,
gasLimit: gasLimit.min_limit,
});

const { unsub, subject } = await this.listenToUserMessageSentEvents(addressHex, programId);

const msgId = await this.sendTx(tx, account);

const replyPayload = await this.waitForReply(subject, msgId);

unsub();

return replyPayload;
}
}
18 changes: 18 additions & 0 deletions js/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Config } from '@jest/types';

const config: Config.InitialOptions = {
clearMocks: true,
coverageProvider: 'v8',
testEnvironment: 'node',
verbose: true,
preset: 'ts-jest/presets/js-with-babel',
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
'^.+\\.tsx?$': ['ts-jest', { useESM: true }],
},
};

export default config;
Loading

0 comments on commit cd0277c

Please sign in to comment.