Skip to content

Commit

Permalink
Add example of generated wCCD client
Browse files Browse the repository at this point in the history
  • Loading branch information
limemloh committed Aug 25, 2023
1 parent 98b2504 commit 783d1f1
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 36 deletions.
74 changes: 74 additions & 0 deletions examples/ccd-js-gen/client-tokenMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { credentials } from '@grpc/grpc-js';
import * as SDK from '@concordium/node-sdk';
import meow from 'meow';
import { parseEndpoint } from '../shared/util';

//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 "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 () => {
// Importing the generated smart contract module client.
// eslint-disable-next-line import/no-unresolved, @typescript-eslint/ban-ts-comment
// @ts-ignore
const wCCDModule = await import('./lib/wCCD').catch((e) => {
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);

contract.dryRun.tokenMetadata(parameter).then((responseHex: string) => {
console.log({ responseHex });
});
})();
25 changes: 17 additions & 8 deletions examples/ccd-js-gen/index.ts → examples/ccd-js-gen/generate.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
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';

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

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
--index, -i The index of the smart contract. Defaults to 2059, which is wCCD on Testnet.
Options
--help, -h Displays this message
Expand All @@ -32,6 +33,7 @@ const cli = meow(
type: 'number',
alias: 'i',
isRequired: true,
default: 2059,
},
subindex: {
type: 'number',
Expand All @@ -53,6 +55,13 @@ const contractAddress: SDK.ContractAddress = {
subindex: BigInt(cli.flags.subindex),
};

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

contract.dryRun.getAccountAddress('');
(async () => {
const info = await grpcClient.getInstanceInfo(contractAddress);
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');
await Gen.generateContractClients(mod, 'wCCD', outDir, {
output: 'Typescript',
});
})();
Binary file removed examples/ccd-js-gen/module.wasm.v1
Binary file not shown.
3 changes: 1 addition & 2 deletions examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"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",
"generate-module": "ccd-js-gen --module ccd-js-gen/module.wasm.v1 --out-dir ccd-js-gen/lib"
"run-example": "node --experimental-specifier-resolution=node --loader ts-node/esm"
}
}
2 changes: 1 addition & 1 deletion packages/ccd-js-gen/bin/ccd-js-gen.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env node
// eslint-disable-next-line @typescript-eslint/no-var-requires
const cli = require('../dist/src/cli.js');
const cli = require('../lib/src/cli.js');
cli.main();
5 changes: 3 additions & 2 deletions packages/ccd-js-gen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"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",
"main": "lib/src/lib.js",
"typings": "lib/src/lib.d.ts",
"scripts": {
"build": "tsc",
"clean": "rm -r lib",
Expand All @@ -29,6 +29,7 @@
"license": "Apache-2.0",
"dependencies": {
"@concordium/common-sdk": "9.0.0",
"buffer": "^6.0.3",
"commander": "^11.0.0",
"ts-morph": "^19.0.0"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/ccd-js-gen/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ export async function main(): Promise<void> {
)
.parse(process.argv);
const options = program.opts<Options>();
await lib.generateContractClients(options.module, options.outDir);
await lib.generateContractClientsFromFile(options.module, options.outDir);
}
61 changes: 48 additions & 13 deletions packages/ccd-js-gen/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,75 @@ 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';
import { Buffer } from 'buffer/';

/** Output options for the generated code */
export type OutputOptions =
| 'Typescript'
| 'Javascript'
| 'TypedJavascript'
| 'Everything';

/** Options for generating clients */
export type GenerateContractClientsOptions = {
output?: OutputOptions;
};

/**
* Generate smart contract client code for a given smart contract module.
* Generate smart contract client code for a given smart contract module file.
* @param modulePath Path to the smart contract module.
* @param outDirPath Path to the directory to use for the output.
* @param options Options for generating the clients.
*/
export async function generateContractClients(
export async function generateContractClientsFromFile(
modulePath: string,
outDirPath: string
outDirPath: string,
options: GenerateContractClientsOptions = {}
): 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 module = SDK.Module.fromRawBytes(Buffer.from(fileBytes));
return generateContractClients(module, outputName, outDirPath, options);
}

/**
* Generate smart contract client code for a given smart contract module.
* @param s Buffer with bytes for the smart contract module.
* @param outName Name for the output file.
* @param outDirPath Path to the directory to use for the output.
* @param options Options for generating the clients.
*/
export async function generateContractClients(
scModule: SDK.Module,
outName: string,
outDirPath: string,
options: GenerateContractClientsOptions = {}
): Promise<void> {
const outputOption = options.output ?? 'Everything';
const moduleInterface = await scModule.parseModuleInterface();
const outputFilePath = path.format({
name: outputName,
dir: outDirPath,
name: outName,
ext: '.ts',
});

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

/** Iterates a module interface adding code to the provided source file. */
Expand Down Expand Up @@ -128,7 +164,6 @@ this.${dryRunId} = new ${contractDryRunClassId}(this.${genericContractId});`
});

for (const entrypointName of contract.entrypointNames) {
const receiveName = `${contract.contractName}.${entrypointName}`;
const transactionMetadataId = 'transactionMetadata';
const parameterId = 'parameter';
const signerId = 'signer';
Expand Down Expand Up @@ -157,7 +192,7 @@ this.${dryRunId} = new ${contractDryRunClassId}(this.${genericContractId});`
})
.setBodyText(
`return this.${genericContractId}.createAndSendUpdateTransaction(
'${receiveName}',
'${entrypointName}',
SDK.encodeHexString,
${transactionMetadataId},
${parameterId},
Expand Down Expand Up @@ -187,7 +222,7 @@ this.${dryRunId} = new ${contractDryRunClassId}(this.${genericContractId});`
})
.setBodyText(
`return this.${genericContractId}.invokeView(
'${receiveName}',
'${entrypointName}',
SDK.encodeHexString,
(hex: SDK.HexString) => hex,
${parameterId},
Expand Down
2 changes: 1 addition & 1 deletion packages/common/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

- `CIS4Contract` class for seemlessly interacting with contracts adhering to the CIS4 standard.
- `Module` class for functionality related to smart contract modules, such as parsing the WebAssembly and interface of the module.
- Smart contract types `ContractName`, `EntrypointName` and helper functions `isInitName`, `isReceiveName`, `getContractNameFromInit` and `getNamesFromReceive`.
- Smart contract related types `ContractName`, `EntrypointName` and helper functions `isInitName`, `isReceiveName`, `getContractNameFromInit` and `getNamesFromReceive`.

## 9.0.0

Expand Down
32 changes: 24 additions & 8 deletions packages/common/src/types/Module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ModuleReference } from './moduleReference';
import * as H from '../contractHelpers';
import { sha256 } from '../hash';
import { Buffer } from 'buffer/';
import { VersionedModuleSource } from '../types';

/** Interface of a smart contract containing the name of the contract and every entrypoint. */
export type ContractInterface = {
Expand All @@ -17,22 +19,36 @@ export type ModuleInterface = Map<H.ContractName, ContractInterface>;
* A versioned smart contract module.
*/
export class Module {
/** The version of the smart contract module. */
public version: number;
/** Cache of the parsed WebAssembly module. */
private wasmModule: WebAssembly.Module | undefined;
/** Cache of the module reference. */
private moduleRef: ModuleReference | undefined;

private constructor(public moduleSource: Buffer) {
this.version = moduleSource.readInt32LE();
private constructor(
/** The version of the smart contract module. */
public version: number,
/** Bytes for the WebAssembly module. */
public moduleSource: Buffer
) {}

/**
* Construct a smart contract module object from bytes, potentially read from a file.
* @param bytes Bytes encoding a versioned smart contract module.
*/
public static fromRawBytes(bytes: Buffer): Module {
const version = bytes.readInt32LE(0);
const moduleSource = bytes.subarray(8);
return new Module(version, Buffer.from(moduleSource));
}

/**
* Construct a smart contract module from the module source.
* Contruct a smart contract module object from a versioned module source.
* @param versionedModule The versioned module.
*/
public static from(moduleSource: Buffer): Module {
return new Module(moduleSource);
public static fromModuleSource(
versionedModule: VersionedModuleSource
): Module {
return new Module(versionedModule.version, versionedModule.source);
}

/** Calculate the module reference from the module source. The module reference is cached. */
Expand All @@ -48,7 +64,7 @@ export class Module {
public getWasmModule(): Promise<WebAssembly.Module> {
return this.wasmModule !== undefined
? Promise.resolve(this.wasmModule)
: WebAssembly.compile(this.moduleSource.subarray(8));
: WebAssembly.compile(this.moduleSource);
}

/**
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1320,6 +1320,7 @@ __metadata:
"@types/node": ^20.5.0
"@typescript-eslint/eslint-plugin": ^4.28.1
"@typescript-eslint/parser": ^4.28.1
buffer: ^6.0.3
commander: ^11.0.0
eslint: ^7.29.0
eslint-config-prettier: ^8.3.0
Expand Down

0 comments on commit 783d1f1

Please sign in to comment.