From ee529c7042a20efacbf695995ec70f88921398f0 Mon Sep 17 00:00:00 2001 From: Mitch Pierias Date: Thu, 25 Apr 2019 10:30:52 +1000 Subject: [PATCH] Documented API methods All API methods have now be documented or updated --- src/accounts/account.ts | 25 ++++++++- src/accounts/accountManager.ts | 72 +++++++++++++++---------- src/accounts/utils.ts | 17 +++++- src/cli/lamington-build.ts | 6 +++ src/cli/lamington-start.ts | 5 ++ src/cli/lamington-stop.ts | 4 ++ src/cli/lamington-test.ts | 6 +++ src/cli/utils.ts | 84 ++++++++++++++++++++++------- src/configManager.ts | 5 +- src/contracts/contract.ts | 5 +- src/contracts/contractDeployer.ts | 84 +++++++++++++++++++++++------ src/contracts/typeGenerator.test.ts | 8 ++- src/contracts/typeGenerator.ts | 43 ++++++++------- 13 files changed, 270 insertions(+), 94 deletions(-) diff --git a/src/accounts/account.ts b/src/accounts/account.ts index bcd5acf..5ddcb53 100644 --- a/src/accounts/account.ts +++ b/src/accounts/account.ts @@ -5,16 +5,21 @@ import { AccountManager } from './accountManager'; export class Account { + /** EOSIO account name */ public name: string; + /** EOSIO account public key */ public publicKey: string; + /** EOSIO account private key */ public privateKey: string; + /** EOSIO account permissions */ public permissions: Permissions; constructor(name: string, privateKey: string, publicKey?: string) { + // Store references this.name = name; this.privateKey = privateKey; this.publicKey = publicKey || ecc.privateToPublic(privateKey); - + // Set default permissions this.permissions = { active: { actor: name, @@ -27,6 +32,11 @@ export class Account { }; } + /** + * Returns a configured active key permission + * @author Kevin Brown + * @returns Valid active key + */ public get active() { return [ { @@ -36,6 +46,11 @@ export class Account { ]; } + /** + * Returns a configured owner key permission + * @author Kevin Brown + * @returns Valid owner key + */ public get owner() { return [ { @@ -45,7 +60,13 @@ export class Account { ]; } - public addCodePermission = async (contract: Contract) => { + /** + * Adds the `eosio.code` permission to this account + * @author Kevin Brown + * @author Mitch Pierias + * @returns Add permission transaction promise + */ + public addCodePermission = async () => { await AccountManager.addCodePermission(this); }; } diff --git a/src/accounts/accountManager.ts b/src/accounts/accountManager.ts index b8dad10..03671d8 100644 --- a/src/accounts/accountManager.ts +++ b/src/accounts/accountManager.ts @@ -5,7 +5,6 @@ import * as ecc from 'eosjs-ecc'; import { Account } from './account'; import { accountNameFromPublicKey } from './utils'; import { EOSManager } from '../eosManager'; -import { Contract } from '../contracts'; interface AccountCreationOptions { creator?: Account; @@ -13,44 +12,63 @@ interface AccountCreationOptions { } export class AccountManager { + + /** + * Generates a new random account + * @note Shorthand method for [[AccountManager.createAccounts]] + * @author Kevin Brown + * @param options Optional account creation settings + * @returns Result returned from [[AccountManager.createAccounts]] + */ static createAccount = async (options?: AccountCreationOptions) => { const [account] = await AccountManager.createAccounts(1, options); - return account; }; + /** + * Generates a specified number of random accounts + * @author Kevin Brown + * @param numberOfAccounts Number of accounts to generate + * @returns Array of created account transaction promises + */ static createAccounts = async (numberOfAccounts = 1, options?: AccountCreationOptions) => { const accounts = []; - + // Repeat account creation for specified for (let i = 0; i < numberOfAccounts; i++) { const privateKey = await ecc.unsafeRandomKey(); const publicKey = await ecc.privateToPublic(privateKey); const accountName = accountNameFromPublicKey(publicKey); const account = new Account(accountName, privateKey); - + // Publish the new account and store result await AccountManager.setupAccount(account, options); - accounts.push(account); } - + // Return created acounts return accounts; }; + /** + * Publishes a new account and allocates ram where possible + * @author Kevin Brown + * @param account [[Account]] to publish + * @param options Optional account settings + * @returns Transaction result promise + */ static setupAccount = async (account: Account, options?: AccountCreationOptions) => { const { creator, eos } = AccountManager.flattenOptions(options); - + // Validate account contains required values if (!account.name) throw new Error('Missing account name.'); if (!account.publicKey) throw new Error('Missing public key.'); if (!account.privateKey) throw new Error('Missing private key.'); - + // Configure the Signature Provider if available if (EOSManager.signatureProvider) { const nonLegacyPublicKey = convertLegacyPublicKey(account.publicKey); EOSManager.signatureProvider.keys.set(nonLegacyPublicKey, account.privateKey); EOSManager.signatureProvider.availableKeys.push(nonLegacyPublicKey); } - + // Get the system contract const systemContract = await eos.getContract('eosio'); - + // Build account creation actions const actions: any = [ { account: 'eosio', @@ -100,7 +118,6 @@ export class AccountManager { }, }); } - // Same deal for delegatebw. Only if it's actually a thing. if (systemContract.actions.has('delegatebw')) { actions.push({ @@ -116,7 +133,7 @@ export class AccountManager { }, }); } - + // Execute the transaction return await EOSManager.transact({ actions }, eos); }; @@ -124,22 +141,21 @@ export class AccountManager { * Grants `eosio.code` permission to the specified account's `active` key * @note Should be moved to the `contracts/contract.ts` I think? * @note Actually it is `account` based and not specific to contracts... - * @author Kevin Brown - * @author Mitch Pierias + * @author Kevin Brown + * @author Mitch Pierias * @param account Account without `eosio.code` permissions */ static addCodePermission = async (account: Account) => { // We need to get their existing permissions, then add in a new eosio.code permission for this contract. const { permissions } = await EOSManager.rpc.get_account(account.name); - const active = permissions.find((permission: any) => permission.perm_name == 'active'); - - const auth = active.required_auth; - const existingPermission = auth.accounts.find( + const { required_auth } = permissions.find((permission: any) => permission.perm_name == 'active'); + // Check if `eosio.code` has already been set + const existingPermission = required_auth.accounts.find( (account: any) => account.permission.actor === account.name && account.permission.permission === 'eosio.code' ); - + // Throw if permission exists if (existingPermission) { throw new Error( `Code permission is already present on account ${account.name} for contract ${ @@ -147,13 +163,12 @@ export class AccountManager { }` ); } - - // Add it in. - auth.accounts.push({ + // Append the `eosio.code` permission to existing + required_auth.accounts.push({ permission: { actor: account.name, permission: 'eosio.code' }, weight: 1, }); - + // Construct the update actions const actions: any = [ { account: 'eosio', @@ -163,16 +178,19 @@ export class AccountManager { account: account.name, permission: 'active', parent: 'owner', - auth, + required_auth, }, }, ]; - - //console.log(JSON.stringify(actions, null, '\t')) - + // Execute the transaction actions await EOSManager.transact({ actions }); }; + /** + * Flattens account creation options + * @author Kevin Brown + * @returns Account creation options + */ private static flattenOptions(options?: AccountCreationOptions) { const creator = (options && options.creator) || EOSManager.adminAccount; const eos = (options && options.eos) || EOSManager.api; diff --git a/src/accounts/utils.ts b/src/accounts/utils.ts index cb2e9ee..552d993 100644 --- a/src/accounts/utils.ts +++ b/src/accounts/utils.ts @@ -1,7 +1,9 @@ import * as ecc from 'eosjs-ecc'; -export const accountNameFromPublicKey = (publicKey: string) => hashToEOSName(ecc.sha256(publicKey)); +/** Digit pattern expression */ +const digitPattern = /[06789]/g; +/** Digit mapping lookup */ const digitMapping: { [key: string]: string } = { '0': '1', '6': '2', @@ -10,8 +12,19 @@ const digitMapping: { [key: string]: string } = { '9': '5', }; -const digitPattern = /[06789]/g; +/** + * Generates an account name from the specified public key + * @author Kevin Brown + * @param publicKey Valid EOSIO public key + * @returns EOSIO account name + */ +export const accountNameFromPublicKey = (publicKey: string) => hashToEOSName(ecc.sha256(publicKey)); +/** + * Generates an account name from a hashed public key + * @author Kevin Brown + * @returns EOSIO account name + */ export const hashToEOSName = (data: string) => `l${data .substring(0, 11) diff --git a/src/cli/lamington-build.ts b/src/cli/lamington-build.ts index 342d8ed..ad4738d 100644 --- a/src/cli/lamington-build.ts +++ b/src/cli/lamington-build.ts @@ -2,6 +2,12 @@ import { eosIsReady, startEos, stopContainer, buildAll } from './utils'; import { GitIgnoreManager } from '../gitignoreManager'; import { ConfigManager } from '../configManager'; +/** + * Executes a contract build procedure + * @note Keep alive setup is incomplete + * @author Kevin Brown + * @author Mitch Pierias + */ const run = async () => { // Initialize configuration await ConfigManager.initWithDefaults(); diff --git a/src/cli/lamington-start.ts b/src/cli/lamington-start.ts index e0989e6..8a4cc1c 100644 --- a/src/cli/lamington-start.ts +++ b/src/cli/lamington-start.ts @@ -1,5 +1,10 @@ import { startEos } from './utils'; +/** + * Starts EOS and throws caught errors + * @note This should handle caught errors + * @author Kevin Brown + */ startEos().catch(error => { throw error; }); diff --git a/src/cli/lamington-stop.ts b/src/cli/lamington-stop.ts index 294cdde..7ca5c0c 100644 --- a/src/cli/lamington-stop.ts +++ b/src/cli/lamington-stop.ts @@ -1,5 +1,9 @@ import { stopContainer } from './utils'; +/** + * Stops the current Lamington docker container + * @author Kevin Brown + */ stopContainer() .then(() => console.log('Lamington container stopped.')) .catch(error => { diff --git a/src/cli/lamington-test.ts b/src/cli/lamington-test.ts index d08ae9f..621b594 100644 --- a/src/cli/lamington-test.ts +++ b/src/cli/lamington-test.ts @@ -2,6 +2,12 @@ import { eosIsReady, startEos, runTests, stopContainer, buildAll } from './utils import { GitIgnoreManager } from '../gitignoreManager'; import { ConfigManager } from '../configManager'; +/** + * Executes a build and test procedure + * @note Keep alive setup is incomplete + * @author Kevin Brown + * @author Mitch Pierias + */ const run = async () => { // Initialize the configuration await ConfigManager.initWithDefaults(); diff --git a/src/cli/utils.ts b/src/cli/utils.ts index f8b7d66..91cf863 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -23,23 +23,36 @@ import { generateTypes } from '../contracts'; import { ConfigManager } from '../configManager'; import * as spinner from './logIndicator'; +/** @hidden Current working directory reference */ const WORKING_DIRECTORY = process.cwd(); +/** @hidden Temporary docker resource directory */ const TEMP_DOCKER_DIRECTORY = path.join(__dirname, '.temp-docker'); +/** @hidden Slowest Expected test duration */ const TEST_EXPECTED_DURATION = 2000; +/** @hidden Maximum test duration */ const TEST_TIMEOUT_DURATION = 10000; +/** @hidden Maximum number of EOS connection attempts before fail */ +const MAX_CONNECTION_ATTEMPTS = 8; +/** + * Extracts the version identifier from a string + * @author Kevin Brown + * @returns Version identifier + */ const versionFromUrl = (url: string) => { - // Looks for strings in this format: - // '/v1.4.6/' - // Allows us to pull just the version part out with the group. + // Looks for strings in this format: `/v1.4.6/` const pattern = /\/(v\d+\.\d+\.\d+)\//g; const result = url.match(pattern); - + // Handle result if (!result) throw new Error(`Could not extract version number from url: '${url}'`); - return result[1]; }; +/** + * Constructs the name of the current Lamington Docker image + * @author Kevin Brown + * @returns Docker image name + */ const dockerImageName = async () => { await ConfigManager.initWithDefaults(); @@ -48,18 +61,26 @@ const dockerImageName = async () => { )}`; }; +/** + * Determines if the docker image exists + * @author Kevin Brown + * @returns Result of search + */ export const imageExists = async () => { + // Fetch image name and check existence const result = await docker.command(`images ${await dockerImageName()}`); - return result.images.length > 0; }; +/** + * Configures and builds the docker image + * @author Kevin Brown + */ export const buildImage = async () => { // Clear the docker directory if it exists. await rimraf(TEMP_DOCKER_DIRECTORY); await mkdirp(TEMP_DOCKER_DIRECTORY); - - // Write a Dockerfile so docker knows what to build. + // Write a Dockerfile so Docker knows what to build. await writeFile( path.join(TEMP_DOCKER_DIRECTORY, 'Dockerfile'), ` @@ -71,19 +92,28 @@ RUN wget ${ConfigManager.eos} && apt-get install -y ./*.deb && rm -f *.deb RUN apt-get clean && rm -rf /tmp/* /var/tmp/* && rm -rf /var/lib/apt/lists/* ` ); - + // Execute docker build process await docker.command(`build -t ${await dockerImageName()} "${TEMP_DOCKER_DIRECTORY}"`); - // Clean up after ourselves. await rimraf(TEMP_DOCKER_DIRECTORY); }; +/** + * Starts the Lamington container + * @author Kevin Brown + */ export const startContainer = async () => { await docker.command( `run --rm --name lamington -d -p 8888:8888 -p 9876:9876 --mount type=bind,src="${WORKING_DIRECTORY}",dst=/opt/eosio/bin/project --mount type=bind,src="${__dirname}/../scripts",dst=/opt/eosio/bin/scripts -w "/opt/eosio/bin/" ${await dockerImageName()} /bin/bash -c "./scripts/init_blockchain.sh"` ); }; +/** + * Stops the current Lamington container + * @author Kevin Brown + * @author Mitch Pierias + * @returns Docker command promise + */ export const stopContainer = () => { spinner.create('Stopping EOS container'); return docker.command('stop lamington') @@ -91,20 +121,36 @@ export const stopContainer = () => { .catch(err => spinner.fail(err)); } -export const untilEosIsReady = async (attempts = 8) => { +/** + * Sleeps the process until the EOS instance is available + * @author Kevin Brown + * @author Mitch Pierias + * @returns Connection success or throws error + */ +export const untilEosIsReady = async (attempts: number = MAX_CONNECTION_ATTEMPTS) => { + spinner.create('Waiting for EOS'); + // Repeat attempts every second until threshold reached let attempt = 0; - while (attempt < attempts) { attempt++; - - if (await eosIsReady()) return true; - + // Check EOS status + if (await eosIsReady()) { + spinner.end(); + return true; + } + // Wait one second await sleep(1000); } - + // Failed to connect within attempt threshold + spinner.fail(`Failed to connect to an EOS instance`); throw new Error(`Could not contact EOS after trying for ${attempts} second(s).`); }; +/** + * Determines if EOS is available using the `get_info` query response + * @author Kevin Brown + * @returns EOS instance availability + */ export const eosIsReady = async () => { try { await axios.get('http://localhost:8888/v1/chain/get_info'); @@ -115,10 +161,11 @@ export const eosIsReady = async () => { }; /** - * Start a new EOSIO docker image + * Starts a new EOSIO docker image * @author Kevin Brown */ export const startEos = async () => { + spinner.create('Starting EOS') // Create build result cache directories await mkdirp(path.join(WORKING_DIRECTORY, '.lamington', 'compiled_contracts')); await mkdirp(path.join(WORKING_DIRECTORY, '.lamington', 'data')); @@ -139,7 +186,6 @@ export const startEos = async () => { } // Start EOSIO try { - spinner.create('Starting EOS'); // Start the EOS docker container await startContainer(); // Pause process until ready @@ -157,7 +203,7 @@ export const startEos = async () => { ==================================================== \n' ); } catch (error) { - console.error('Could not start EOS blockchain. Error: ', error); + spinner.fail('Could not start EOS blockchain. Error: '+error); process.exit(1); } }; diff --git a/src/configManager.ts b/src/configManager.ts index ead90e6..274c4fb 100644 --- a/src/configManager.ts +++ b/src/configManager.ts @@ -34,10 +34,7 @@ export interface LamingtonConfig { */ export class ConfigManager { - /** - * EOSIO and EOSIO.CDT configuration settings - * @author Kevin Brown - */ + /** @hidden EOSIO and EOSIO.CDT configuration settings */ private static config: LamingtonConfig; /** diff --git a/src/contracts/contract.ts b/src/contracts/contract.ts index 42015c9..a0f8bdf 100644 --- a/src/contracts/contract.ts +++ b/src/contracts/contract.ts @@ -14,7 +14,7 @@ export interface ContractActionOptions { } /** - * Extends + * Adds additional functionality to the EOSJS `Contract` class */ export class Contract implements EOSJSContract { @@ -33,6 +33,7 @@ export class Contract implements EOSJSContract { /** * Gets the currently configured contract account + * @author Kevin Brown * @returns Current contract account */ public get account(): Account { @@ -41,6 +42,7 @@ export class Contract implements EOSJSContract { /** * Gets the current contract identifier + * @author Kevin Brown * @returns Contract identifier */ public get identifier(): string { @@ -122,6 +124,7 @@ export class Contract implements EOSJSContract { /** * Retrieves table rows with the specified table name and optional scope + * @author Kevin Brown * @note Implements a temporary patch for the EOSjs `bool` mapping error * @param table The table name * @param scope Optional table scope, defaults to the table name diff --git a/src/contracts/contractDeployer.ts b/src/contracts/contractDeployer.ts index f794cfb..310dd92 100644 --- a/src/contracts/contractDeployer.ts +++ b/src/contracts/contractDeployer.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import { readFile as readFileCallback } from 'fs'; import { promisify } from 'util'; import { Serialize } from 'eosjs'; +import * as ecc from 'eosjs-ecc'; const readFile = promisify(readFileCallback); @@ -9,35 +10,50 @@ import { Contract } from './contract'; import { Account, AccountManager } from '../accounts'; import { EOSManager } from '../eosManager'; +/** + * Provides a set of methods to manage contract deployment + */ export class ContractDeployer { - public static async deployAtName( - account: Account, - contractIdentifier: string - ) { + + /** + * Deploys contract files to a specified account + * + * ```typescript + * // Create a new account + * const account = await AccountManager.createAccount(); + * // Deploy the contract `mycontract` to the account + * ContractDeployer.deployToAccount('mycontract', account); + * ``` + * @author Kevin Brown + * @param contractIdentifier Contract identifier, typically the contract filename minus the extension + * @param account Account to apply contract code + * @returns Deployed contract instance + */ + public static async deployToAccount(contractIdentifier: string, account: Account) { + // Initialize the serialization buffer const buffer = new Serialize.SerialBuffer({ textEncoder: EOSManager.api.textEncoder, textDecoder: EOSManager.api.textDecoder, }); - + // Construct resource paths const abiPath = path.join('.lamington', 'compiled_contracts', `${contractIdentifier}.abi`); const wasmPath = path.join('.lamington', 'compiled_contracts', `${contractIdentifier}.wasm`); - + // Read resources files for paths let abi = JSON.parse(await readFile(abiPath, 'utf8')); const wasm = await readFile(wasmPath); - + // Extract ABI types const abiDefinition = EOSManager.api.abiTypes.get(`abi_def`); - - // We need to make sure the abi has every field in abiDefinition.fields - // otherwise serialize throws + // Validate ABI definitions returned if (!abiDefinition) throw new Error('Could not retrieve abiDefinition from EOS API when flattening ABIs.'); - + // Ensure ABI contains all fields from `abiDefinition.fields` abi = abiDefinition.fields.reduce( (acc, { name: fieldName }) => Object.assign(acc, { [fieldName]: acc[fieldName] || [] }), abi ); + // Serialize ABI type definitions abiDefinition.serialize(buffer, abi); - + // Set the contract code for the account await EOSManager.transact({ actions: [ { @@ -62,15 +78,51 @@ export class ContractDeployer { }, ], }); - + // Fetch the contract actions and types const { actions, types } = await EOSManager.api.getContract(account.name); - + // Return our newly deployed contract instance return new Contract(EOSManager.api, contractIdentifier, account, abi, actions, types) as T; } - public static async deployClean(contractIdentifier: string) { + /** + * Deploys contract files to a randomly generated account + * + * ```typescript + * // Deploy the contract with identifier + * ContractDeployer.deploy('mycontract'); + * ``` + * + * @author Kevin Brown + * @param contractIdentifier Contract identifier, typically the contract filename minus the extension + * @returns Deployed contract instance + */ + public static async deploy(contractIdentifier: string) { + // Create a new account const account = await AccountManager.createAccount(); + // Call the deployToAccount method with the account + return await ContractDeployer.deployToAccount(contractIdentifier, account); + } - return await ContractDeployer.deployAtName(account, contractIdentifier); + /** + * Deploys contract files to a specified account name + * + * ```typescript + * // Deploy the `mycontract` contract to the account with name `mycontractname` + * ContractDeployer.deployToAccount('mycontract', 'mycontractname'); + * ``` + * + * @note Generating a random private key is not safe + * @author Mitch Pierias + * @param contractIdentifier Contract identifier, typically the contract filename minus the extension + * @param accountName Account name + * @returns Deployed contract instance + */ + public static async deployWithName(contractIdentifier: string, accountName: string) { + // Generate a random private key + const privateKey = await ecc.unsafeRandomKey(); + // Initialize account with name + const account = new Account(accountName, privateKey); + // Call the deployToAccount method with the account + return await ContractDeployer.deployToAccount(contractIdentifier, account); } } diff --git a/src/contracts/typeGenerator.test.ts b/src/contracts/typeGenerator.test.ts index e90a7f7..736e1f2 100644 --- a/src/contracts/typeGenerator.test.ts +++ b/src/contracts/typeGenerator.test.ts @@ -4,8 +4,7 @@ import { } from './typeGenerator'; /** - * Big Number Types - * @desc Javascript only supports number, so CPP integer types need to be mapped accordingly + * Javascript only supports number, so CPP integer types need to be mapped accordingly */ const numberTypes = [ 'int8','int16','int32','int64','int128','int256', @@ -14,14 +13,13 @@ const numberTypes = [ ]; /** - * EOS Name Types - * @desc Name types are typically a string or uint64_t and typically represent an identity on the EOS blockchain + * Name types are typically a string or uint64_t and typically represent an identity on the EOS blockchain */ const stringNumberTypes = ['name','action_name','scope_name','account_name','permission_name','table_name']; describe("type generator", () => { - context('map paramater types', () => { + context('map parameter types', () => { it(`should map 'string' to 'string'`, () => { assert.equal(mapParameterType('string'), 'string', `'string' types should map to 'string'`) diff --git a/src/contracts/typeGenerator.ts b/src/contracts/typeGenerator.ts index f8fe72a..766cdfd 100644 --- a/src/contracts/typeGenerator.ts +++ b/src/contracts/typeGenerator.ts @@ -7,6 +7,11 @@ import * as spinner from './../cli/logIndicator'; const glob = promisify(globWithCallbacks); +/** + * Transforms a string into the pascal-case format + * @author Kevin Brown + * @param value String for case transformation + */ const pascalCase = (value: string) => { const snakePattern = /[_.]+./g; const upperFirst = value[0].toUpperCase() + value.slice(1); @@ -55,7 +60,7 @@ export const generateAllTypes = async () => { export const generateTypes = async (contractIdentifier: string) => { // Notify generating has begun spinner.create(`Generating types definitions`); - + // Create contract details const contractName = path.basename(contractIdentifier); const abiPath = path.join('.lamington', 'compiled_contracts', `${contractIdentifier}.abi`); const abi = JSON.parse(fs.readFileSync(path.resolve(abiPath), 'utf8')); @@ -74,14 +79,13 @@ export const generateTypes = async (contractIdentifier: string) => { '// =====================================================', '', ]; - // Imports + // Define imports const imports = ['Account', 'Contract']; if (contractTables.length > 0) imports.push('TableRowsResult'); - + // Generate import definitions result.push(`import { ${imports.join(', ')} } from 'lamington';`); result.push(''); result.push('// Table row types'); - // Generate table row types from ABI for (const table of contractTables) { const tableInterface = { @@ -99,20 +103,19 @@ export const generateTypes = async (contractIdentifier: string) => { const parameters = contractStructs[action.name].fields.map( (parameter: any) => `${parameter.name}: ${mapParameterType(parameter.type)}` ); - // Optional parameter at the end on every contract method. parameters.push('options?: { from?: Account }'); - + return `${action.name}(${parameters.join(', ')}): Promise;`; }); - + // Generate tables const generatedTables = contractTables.map( (table: any) => `${table.name}(scope?: string): Promise>;` ); - + // Generate the contract interface with actions and tables const contractInterface = { [`export interface ${pascalCase(contractName)} extends Contract`]: [ '// Actions', @@ -122,20 +125,24 @@ export const generateTypes = async (contractIdentifier: string) => { ...generatedTables, ], }; - + // Cache contract result result.push(contractInterface); result.push(''); - + // Save generated contract await saveInterface(contractIdentifier, result); spinner.end(''); }; -const saveInterface = async ( - contractIdentifier: string, - contract: GeneratorLevel | IndentedGeneratorLevel -) => { +/** + * Writes the contract interface to file + * @author Kevin Brown + * @param contractIdentifier Path to file without extension + * @param interfaceContent Generated contract interface + */ +const saveInterface = async (contractIdentifier: string, interfaceContent: GeneratorLevel | IndentedGeneratorLevel) => { + // Open a write stream to file const file = fs.createWriteStream(`${contractIdentifier}.ts`); - + // Write formatted blocks let indentLevel = 0; const write = (value: string) => file.write('\t'.repeat(indentLevel) + value + '\n'); const writeIndented = (level: IndentedGeneratorLevel) => { @@ -149,6 +156,7 @@ const saveInterface = async ( write('}'); } }; + // Write block content or indent again const writeLevel = (level: GeneratorLevel | IndentedGeneratorLevel) => { if (Array.isArray(level)) { for (const entry of level) { @@ -162,8 +170,7 @@ const saveInterface = async ( writeIndented(level); } }; - - writeLevel(contract); - + // Write interface to file and close + writeLevel(interfaceContent); file.close(); };