Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial set up of ccd-js-gen package #242

Merged
merged 10 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ jobs:
./packages/nodejs/lib
./packages/common/grpc
./packages/nodejs/grpc
./packages/ccd-js-gen/lib

typecheck-examples:
runs-on: ubuntu-22.04
Expand Down Expand Up @@ -158,6 +159,7 @@ jobs:

lint:
runs-on: ubuntu-22.04
needs: build
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v3
Expand All @@ -167,6 +169,11 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
cache: yarn

- name: Get build-debug
uses: ./.github/actions/download-artifact
with:
name: build-debug

- name: Install dependencies
run: yarn install --immutable

Expand Down
77 changes: 77 additions & 0 deletions examples/ccd-js-gen/wCCD/client-tokenMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { credentials } from '@grpc/grpc-js';
import * as SDK from '@concordium/node-sdk';
import meow from 'meow';
import { parseEndpoint } from '../../shared/util';

// The generated module could be imported directly like below,
// but for this example it is imported dynamicly to improve
// the error message when not generated.
// import * as wCCDModule from './lib/wCCD';

const cli = meow(
`
This example uses a generated smart contract client for the wCCD smart contract.

Usage
$ yarn run-example <path-to-this-file> [options]

Required
--index, -i The index of the smart contract. Defaults to 2059, which is wCCD on testnet.

Options
--help, -h Displays this message
--endpoint, -e Specify endpoint of a grpc2 interface of a Concordium node in the format "<scheme>://<address>:<port>". Defaults to 'http://localhost:20000'
--subindex, The subindex of the smart contract. Defaults to 0
`,
{
importMeta: import.meta,
flags: {
endpoint: {
type: 'string',
alias: 'e',
default: 'http://localhost:20000',
},
index: {
type: 'number',
alias: 'i',
default: 2059,
},
subindex: {
type: 'number',
default: 0,
},
},
}
);

const [address, port, scheme] = parseEndpoint(cli.flags.endpoint);
const grpcClient = SDK.createConcordiumClient(
address,
Number(port),
scheme === 'https' ? credentials.createSsl() : credentials.createInsecure()
);

const contractAddress: SDK.ContractAddress = {
index: BigInt(cli.flags.index),
subindex: BigInt(cli.flags.subindex),
};

(async () => {
// Importing the generated smart contract module client.
/* eslint-disable import/no-unresolved */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const wCCDModule = await import('./lib/wCCD').catch((e) => {
/* eslint-enable import/no-unresolved */
console.error(
'\nFailed to load the generated wCCD module, did you run the `generate` script?\n'
);
throw e;
});

const parameter = '010000'; // First 2 bytes for number of tokens to query, 1 byte for the token ID.
const contract = new wCCDModule.Cis2WCCD(grpcClient, contractAddress);

const responseHex = await contract.dryRun.tokenMetadata(parameter);
console.log({ responseHex });
})();
73 changes: 73 additions & 0 deletions examples/ccd-js-gen/wCCD/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { credentials } from '@grpc/grpc-js';
import * as SDK from '@concordium/node-sdk';
import * as Gen from '@concordium/ccd-js-gen';
import * as Path from 'node:path';
import * as Url from 'node:url';
import meow from 'meow';
import { parseEndpoint } from '../../shared/util';

const cli = meow(
`
This example fetches the wCCD smart contract module source from the chain and generates a typescript client for interacting with such a smart contract.

Usage
$ yarn run-example <path-to-this-file> [options]

Required
--index, -i The index of the smart contract. Defaults to 2059, which is wCCD on Testnet.

Options
--help, -h Displays this message
--endpoint, -e Specify endpoint of a grpc2 interface of a Concordium node in the format "address:port". Defaults to 'localhost:20000'
--subindex, The subindex of the smart contract. Defaults to 0
`,
{
importMeta: import.meta,
flags: {
endpoint: {
type: 'string',
alias: 'e',
default: 'localhost:20000',
},
index: {
type: 'number',
alias: 'i',
isRequired: true,
default: 2059,
},
subindex: {
type: 'number',
default: 0,
},
},
}
);

const [address, port] = parseEndpoint(cli.flags.endpoint);
const grpcClient = SDK.createConcordiumClient(
address,
Number(port),
credentials.createInsecure()
);

const contractAddress: SDK.ContractAddress = {
index: BigInt(cli.flags.index),
subindex: BigInt(cli.flags.subindex),
};

(async () => {
console.info(`Fetching instance information for ${contractAddress.index}.`);
const info = await grpcClient.getInstanceInfo(contractAddress);
console.info(
`Fetching smart contract module source with reference '${info.sourceModule.moduleRef}'.`
);
const moduleSource = await grpcClient.getModuleSource(info.sourceModule);
const mod = SDK.Module.fromModuleSource(moduleSource);
const filePath = Url.fileURLToPath(import.meta.url);
const outDir = Path.join(Path.dirname(filePath), 'lib');
console.info(`Generating smart contract module client at '${outDir}'.`);
await Gen.generateContractClients(mod, 'wCCD', outDir, {
output: 'TypeScript',
});
console.info('Code generation was successful.');
})();
2 changes: 2 additions & 0 deletions examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"name": "@concordium/examples",
"type": "module",
"dependencies": {
"@concordium/ccd-js-gen": "workspace:^",
"@concordium/common-sdk": "workspace:^",
"@concordium/node-sdk": "workspace:^",
"@grpc/grpc-js": "^1.3.4",
"@noble/ed25519": "^1.7.1",
Expand Down
26 changes: 21 additions & 5 deletions examples/shared/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,27 @@ export const parseAddress = (input: string): Base58String | ContractAddress => {
return { index: BigInt(i), subindex: BigInt(si) };
};

export const parseEndpoint = (endpoint: string): [string, number] => {
// Regular expression for matching the scheme prefix of a URL.
const schemeRegex = /^(\w+):\/\//;

/**
* Parse endpoint information from a string, such as 'http://my-concordium-node:20000'
* @param endpoint String with information of an endpoint.
* @returns Triple with ['<address>', <port>, '<scheme>'].
*/
export const parseEndpoint = (
endpoint: string
): [string, number, string | undefined] => {
const result = schemeRegex.exec(endpoint);
const matched = result?.[0];
const scheme = result?.[1];

const noSchemeEndpoint = endpoint.substring(matched?.length ?? 0);

// Split endpoint on last colon
const lastColonIndex = endpoint.lastIndexOf(':');
const address = endpoint.substring(0, lastColonIndex);
const port = Number(endpoint.substring(lastColonIndex + 1));
const lastColonIndex = noSchemeEndpoint.lastIndexOf(':');
const address = noSchemeEndpoint.substring(0, lastColonIndex);
const port = Number(noSchemeEndpoint.substring(lastColonIndex + 1));

return [address, port];
return [address, port, scheme];
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"./packages/common",
"./packages/nodejs",
"./packages/web",
"./packages/ccd-js-gen",
"./examples"
]
},
Expand Down
4 changes: 4 additions & 0 deletions packages/ccd-js-gen/bin/ccd-js-gen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env node
// eslint-disable-next-line @typescript-eslint/no-var-requires
const cli = require('../lib/src/cli.js');
cli.main();
52 changes: 52 additions & 0 deletions packages/ccd-js-gen/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@concordium/ccd-js-gen",
"version": "1.0.0",
"description": "Generate JS clients for the Concordium Blockchain",
"bin": "bin/ccd-js-gen.js",
"main": "lib/src/lib.js",
"typings": "lib/src/lib.d.ts",
"scripts": {
"build": "tsc",
"build-dev": "yarn build",
"clean": "rm -r lib",
"lint": "eslint src/** bin/** --cache --ext .ts,.js --max-warnings 0",
"lint-fix": "yarn --silent lint --fix; exit 0",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"concordium",
"smart-contracts"
],
"repository": {
"type": "git",
"url": "https://github.com/Concordium/concordium-node-sdk-js",
"directory": "packages/ccd-js-gen"
},
"author": {
"name": "Concordium Software",
"email": "[email protected]",
"url": "https://concordium.com"
},
"license": "Apache-2.0",
"dependencies": {
"@concordium/common-sdk": "9.3.0",
"buffer": "^6.0.3",
"commander": "^11.0.0",
"ts-morph": "^19.0.0"
},
"devDependencies": {
"@types/node": "^20.5.0",
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.28.1",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-prettier": "^3.4.0",
"prettier": "^2.3.2",
"typescript": "^4.3.5"
},
"prettier": {
"singleQuote": true,
"tabWidth": 4
}
}
34 changes: 34 additions & 0 deletions packages/ccd-js-gen/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
This file contains code for building the command line inferface to the ccd-js-gen library.
*/
import { Command } from 'commander';
import packageJson from '../package.json';
import * as lib from './lib';

/** Type representing the CLI options/arguments and needs to match the options set with commander.js */
type Options = {
/** Smart contract module to generate clients from */
module: string;
/** The output directory for the generated code */
outDir: string;
};

// Main function, which is called be the executable script in `bin`.
export async function main(): Promise<void> {
const program = new Command();
program
.name(packageJson.name)
.description(packageJson.description)
.version(packageJson.version)
.requiredOption(
'-m, --module <module-file>',
'Smart contract module to generate clients from'
)
.requiredOption(
'-o, --out-dir <directory>',
'The output directory for the generated code'
)
.parse(process.argv);
const options = program.opts<Options>();
await lib.generateContractClientsFromFile(options.module, options.outDir);
}
Loading