Skip to content

Commit

Permalink
Initial set up of ccd-js-gen package
Browse files Browse the repository at this point in the history
  • Loading branch information
limemloh committed Aug 25, 2023
1 parent 1defce6 commit 1a2ced7
Show file tree
Hide file tree
Showing 14 changed files with 648 additions and 3 deletions.
58 changes: 58 additions & 0 deletions examples/ccd-js-gen/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { credentials } from '@grpc/grpc-js';
import * as SDK from '@concordium/node-sdk';
import meow from 'meow';
import { parseEndpoint } from '../shared/util';

// Importing the generated smart contract module client.
// eslint-disable-next-line import/no-unresolved
import * as Module from './lib/module';

const cli = meow(
`
Usage
$ yarn run-example <path-to-this-file> [options]
Required
--index, -i The index of the smart contract
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,
},
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),
};

const contract = new Module.SmartContractTestBench(grpcClient, contractAddress);

contract.dryRun.getAccountAddress('');
Binary file added examples/ccd-js-gen/module.wasm.v1
Binary file not shown.
5 changes: 4 additions & 1 deletion 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 All @@ -22,6 +24,7 @@
"lint": "eslint . --cache --ext .ts,.tsx --max-warnings 0",
"lint-fix": "yarn lint --fix; exit 0",
"typecheck": "tsc --noEmit",
"run-example": "node --experimental-specifier-resolution=node --loader ts-node/esm"
"run-example": "node --experimental-specifier-resolution=node --loader ts-node/esm",
"generate-module": "ccd-js-gen --module ccd-js-gen/module.wasm.v1 --out-dir ccd-js-gen/lib"
}
}
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('../dist/src/cli.js');
cli.main();
50 changes: 50 additions & 0 deletions packages/ccd-js-gen/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@concordium/ccd-js-gen",
"version": "1.0.0",
"description": "Generate JS clients for the Concordium Blockchain",
"bin": "bin/ccd-js-gen.js",
"main": "dist/src/lib.js",
"typings": "dist/src/lib.d.ts",
"scripts": {
"build": "tsc",
"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.0.0",
"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
}
}
31 changes: 31 additions & 0 deletions packages/ccd-js-gen/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Command } from 'commander';
import packageJson from '../package.json';
import * as lib from './lib';

type Options = {
module: string;
outDir: string;
};

export async function main(): Promise<void> {
const program = new Command();

program
.name(packageJson.name)
.description(packageJson.description)
.version(packageJson.version);

program
.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.generateContractClients(options.module, options.outDir);
}
217 changes: 217 additions & 0 deletions packages/ccd-js-gen/src/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as tsm from 'ts-morph';
import * as SDK from '@concordium/common-sdk';

/**
* Generate smart contract client code for a given smart contract module.
* @param modulePath Path to the smart contract module.
* @param outDirPath Path to the directory to use for the output.
*/
export async function generateContractClients(
modulePath: string,
outDirPath: string
): Promise<void> {
// TODO catch if file does not exist and produce better error.
const fileBytes = await fs.readFile(modulePath);
const module = SDK.Module.from(fileBytes);

const moduleInterface = await module.parseModuleInterface();

const outputName = path.basename(modulePath, '.wasm.v1');
const outputFilePath = path.format({
name: outputName,
dir: outDirPath,
ext: '.ts',
});

const compilerOptions: tsm.CompilerOptions = {
outDir: outDirPath,
declaration: true,
};
const project = new tsm.Project({ compilerOptions });
const sourceFile = project.createSourceFile(outputFilePath, '', {
overwrite: true,
});
addModuleClients(sourceFile, moduleInterface);
await Promise.all([project.save(), project.emit()]);
}

/** Iterates a module interface adding code to the provided source file. */
function addModuleClients(
sourceFile: tsm.SourceFile,
moduleInterface: SDK.ModuleInterface
) {
sourceFile.addImportDeclaration({
namespaceImport: 'SDK',
moduleSpecifier: '@concordium/common-sdk',
});

for (const contract of moduleInterface.values()) {
const contractNameId = 'contractName';
const genericContractId = 'genericContract';
const grpcClientId = 'grpcClient';
const contractAddressId = 'contractAddress';
const dryRunId = 'dryRun';
const contractClassId = toPascalCase(contract.contractName);
const contractDryRunClassId = `${contractClassId}DryRun`;

const classDecl = sourceFile.addClass({
docs: ['Smart contract client for a contract instance on chain.'],
isExported: true,
name: contractClassId,
properties: [
{
docs: [
'Name of the smart contract supported by this client.',
],
scope: tsm.Scope.Public,
isReadonly: true,
name: contractNameId,
type: 'string',
initializer: `'${contract.contractName}'`,
},
{
docs: ['Generic contract client used internally.'],
scope: tsm.Scope.Private,
name: genericContractId,
type: 'SDK.Contract',
},
{
docs: ['Dry run entrypoints of the smart contract.'],
scope: tsm.Scope.Public,
name: dryRunId,
type: contractDryRunClassId,
},
],
});

const dryRunClassDecl = sourceFile.addClass({
docs: [
`Smart contract client for dry running messages to a contract instance of '${contract.contractName}' on chain.`,
],
isExported: true,
name: contractDryRunClassId,
});

classDecl
.addConstructor({
docs: ['Contruct a client for a contract instance on chain'],
parameters: [
{
name: grpcClientId,
type: 'SDK.ConcordiumGRPCClient',
scope: tsm.Scope.Public,
},
{
name: contractAddressId,
type: 'SDK.ContractAddress',
isReadonly: true,
scope: tsm.Scope.Public,
},
],
})
.setBodyText(
`this.${genericContractId} = new SDK.Contract(${grpcClientId}, ${contractAddressId}, '${contract.contractName}');
this.${dryRunId} = new ${contractDryRunClassId}(this.${genericContractId});`
);

dryRunClassDecl.addConstructor({
docs: ['Contruct a client for a contract instance on chain'],
parameters: [
{
name: genericContractId,
type: 'SDK.Contract',
scope: tsm.Scope.Private,
},
],
});

for (const entrypointName of contract.entrypointNames) {
const receiveName = `${contract.contractName}.${entrypointName}`;
const transactionMetadataId = 'transactionMetadata';
const parameterId = 'parameter';
const signerId = 'signer';
classDecl
.addMethod({
docs: [
`Send a message to the '${entrypointName}' entrypoint of the '${contract.contractName}' contract.`,
],
scope: tsm.Scope.Public,
name: toCamelCase(entrypointName),
parameters: [
{
name: transactionMetadataId,
type: 'SDK.ContractTransactionMetadata',
},
{
name: parameterId,
type: 'SDK.HexString',
},
{
name: signerId,
type: 'SDK.AccountSigner',
},
],
returnType: 'Promise<SDK.HexString>',
})
.setBodyText(
`return this.${genericContractId}.createAndSendUpdateTransaction(
'${receiveName}',
SDK.encodeHexString,
${transactionMetadataId},
${parameterId},
${signerId}
);`
);
const blockHashId = 'blockHash';
dryRunClassDecl
.addMethod({
docs: [
`Dry run a message to the '${entrypointName}' entrypoint of the '${contract.contractName}' contract`,
],
scope: tsm.Scope.Public,
name: toCamelCase(entrypointName),
parameters: [
{
name: parameterId,
type: 'SDK.HexString',
},
{
name: blockHashId,
type: 'SDK.HexString',
hasQuestionToken: true,
},
],
returnType: 'Promise<SDK.HexString>',
})
.setBodyText(
`return this.${genericContractId}.invokeView(
'${receiveName}',
SDK.encodeHexString,
(hex: SDK.HexString) => hex,
${parameterId},
${blockHashId}
);`
);
}
}
}

/** Make the first character in a string uppercase */
function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.substring(1);
}

/** Convert a string in snake_case or kebab-case into camelCase. */
function toCamelCase(str: string): string {
return str
.split(/[-_]/g)
.map((word, index) => (index === 0 ? word : capitalize(word)))
.join('');
}

/** Convert a string in snake_case or kebab-case into PascalCase. */
function toPascalCase(str: string): string {
return str.split(/[-_]/g).map(capitalize).join('');
}
4 changes: 4 additions & 0 deletions packages/ccd-js-gen/tsconfig.eslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"include": ["./src/**/*", "bin/**/*"]
}
Loading

0 comments on commit 1a2ced7

Please sign in to comment.