-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial set up of ccd-js-gen package
- Loading branch information
Showing
14 changed files
with
648 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(''); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"extends": "./tsconfig.json", | ||
"include": ["./src/**/*", "bin/**/*"] | ||
} |
Oops, something went wrong.