From 4ec9a31841396474dec02465272f2fe3bd70d62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Holm=20Gj=C3=B8rup?= Date: Mon, 11 Sep 2023 17:41:56 +0200 Subject: [PATCH] Restructure generated clients to be tree-shakable --- .../ccd-js-gen/wCCD/client-tokenMetadata.ts | 16 +- packages/ccd-js-gen/src/lib.ts | 569 +++++++++--------- packages/common/CHANGELOG.md | 14 +- packages/common/src/AccountSequenceNumber.ts | 23 + packages/common/src/BlockHash.ts | 54 ++ packages/common/src/ContractName.ts | 68 +++ packages/common/src/DeployedModule.ts | 155 ----- packages/common/src/Energy.ts | 27 + packages/common/src/GenericContract.ts | 18 +- packages/common/src/InitName.ts | 58 ++ packages/common/src/ModuleClient.ts | 172 ++++++ packages/common/src/Parameter.ts | 57 ++ packages/common/src/TransactionHash.ts | 50 ++ packages/common/src/contractHelpers.ts | 19 +- packages/common/src/index.ts | 10 +- packages/common/src/types.ts | 5 +- .../common/src/types/VersionedModuleSource.ts | 24 +- packages/common/src/types/chainUpdate.ts | 2 +- packages/common/src/types/moduleReference.ts | 4 +- packages/common/src/util.ts | 16 +- 20 files changed, 893 insertions(+), 468 deletions(-) create mode 100644 packages/common/src/AccountSequenceNumber.ts create mode 100644 packages/common/src/BlockHash.ts create mode 100644 packages/common/src/ContractName.ts delete mode 100644 packages/common/src/DeployedModule.ts create mode 100644 packages/common/src/Energy.ts create mode 100644 packages/common/src/InitName.ts create mode 100644 packages/common/src/ModuleClient.ts create mode 100644 packages/common/src/Parameter.ts create mode 100644 packages/common/src/TransactionHash.ts diff --git a/examples/ccd-js-gen/wCCD/client-tokenMetadata.ts b/examples/ccd-js-gen/wCCD/client-tokenMetadata.ts index c121e0004..9ac593456 100644 --- a/examples/ccd-js-gen/wCCD/client-tokenMetadata.ts +++ b/examples/ccd-js-gen/wCCD/client-tokenMetadata.ts @@ -61,7 +61,7 @@ const contractAddress: SDK.ContractAddress = { /* eslint-disable import/no-unresolved */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const wCCDModule = await import('./lib/wCCD').catch((e) => { + const wCCDContractClient = await import('./lib/cis2_wCCD').catch((e) => { /* eslint-enable import/no-unresolved */ console.error( '\nFailed to load the generated wCCD module, did you run the `generate` script?\n' @@ -69,12 +69,18 @@ const contractAddress: SDK.ContractAddress = { throw e; }); - const parameter = '010000'; // First 2 bytes for number of tokens to query, 1 byte for the token ID. - const contract = await wCCDModule.Cis2WCCD.create( + const parameter = SDK.Parameter.fromHexString('010000'); // First 2 bytes for number of tokens to query, 1 byte for the token ID. + const contract = await wCCDContractClient.create( grpcClient, contractAddress ); - const responseHex = await contract.dryRun.tokenMetadata(parameter); - console.log({ responseHex }); + const result = await wCCDContractClient.dryRunTokenMetadata( + contract, + new SDK.AccountAddress( + '357EYHqrmMiJBmUZTVG5FuaMq4soAhgtgz6XNEAJaXHW3NHaUf' + ), + parameter + ); + console.log({ result }); })(); diff --git a/packages/ccd-js-gen/src/lib.ts b/packages/ccd-js-gen/src/lib.ts index e23a632b0..90cdc7984 100644 --- a/packages/ccd-js-gen/src/lib.ts +++ b/packages/ccd-js-gen/src/lib.ts @@ -65,11 +65,6 @@ export async function generateContractClients( options: GenerateContractClientsOptions = {} ): Promise { const outputOption = options.output ?? 'Everything'; - const outputFilePath = path.format({ - dir: outDirPath, - name: outName, - ext: '.ts', - }); const compilerOptions: tsm.CompilerOptions = { outDir: outDirPath, @@ -77,10 +72,9 @@ export async function generateContractClients( outputOption === 'Everything' || outputOption === 'TypedJavaScript', }; const project = new tsm.Project({ compilerOptions }); - const sourceFile = project.createSourceFile(outputFilePath, '', { - overwrite: true, - }); - await addModuleClients(sourceFile, moduleSource); + + await addModuleClient(project, outName, outDirPath, moduleSource); + if (outputOption === 'Everything' || outputOption === 'TypeScript') { await project.save(); } @@ -93,99 +87,111 @@ export async function generateContractClients( } } -/** Iterates a module interface adding code to the provided source file. */ -async function addModuleClients( - sourceFile: tsm.SourceFile, - scModule: SDK.VersionedModuleSource +/** Iterates a module interface building source files in the project. */ +async function addModuleClient( + project: tsm.Project, + outModuleName: string, + outDirPath: string, + moduleSource: SDK.VersionedModuleSource ) { - const moduleInterface = await SDK.parseModuleInterface(scModule); - const moduleRef = await SDK.calculateModuleReference(scModule); - const grpcClientId = 'grpcClient'; - const moduleRefId = 'moduleReference'; - const moduleClientId = 'ModuleClient'; - const deployedModuleId = 'deployedModule'; + const moduleInterface = await SDK.parseModuleInterface(moduleSource); + const moduleRef = await SDK.calculateModuleReference(moduleSource); - sourceFile.addImportDeclaration({ + const outputFilePath = path.format({ + dir: outDirPath, + name: outModuleName, + ext: '.ts', + }); + const moduleSourceFile = project.createSourceFile(outputFilePath, '', { + overwrite: true, + }); + moduleSourceFile.addImportDeclaration({ namespaceImport: 'SDK', moduleSpecifier: '@concordium/common-sdk', }); + const moduleRefId = 'moduleReference'; - sourceFile.addVariableStatement({ + moduleSourceFile.addVariableStatement({ isExported: true, declarationKind: tsm.VariableDeclarationKind.Const, + docs: [ + 'The reference of the smart contract module supported by the provided client.', + ], declarations: [ { + name: moduleRefId, type: 'SDK.ModuleReference', initializer: `new SDK.ModuleReference('${moduleRef.moduleRef}')`, - name: moduleRefId, }, ], }); - const moduleClassDecl = sourceFile.addClass({ + const moduleClientType = `${toPascalCase(outModuleName)}Module`; + const internalModuleClientId = 'internalModuleClient'; + + const moduleClassDecl = moduleSourceFile.addClass({ docs: [ - 'Smart contract module client, can be used for instantiating new smart contract instance on chain.', + `Client for an on-chain smart contract module with module reference '${moduleRef.moduleRef}', can be used for instantiating new smart contract instances.`, ], - isExported: true, - name: moduleClientId, + name: moduleClientType, properties: [ { - docs: ['The reference of this module.'], - isStatic: true, - scope: tsm.Scope.Public, - isReadonly: true, - name: moduleRefId, - initializer: moduleRefId, + docs: [ + 'Having a private field prevents similar structured objects to be considered the same type (similar to nominal typing).', + ], + scope: tsm.Scope.Private, + name: '__nominal', + initializer: 'true', }, { - docs: ['The gRPC connection used by this client.'], + docs: ['Generic module client used internally.'], scope: tsm.Scope.Public, isReadonly: true, - name: grpcClientId, - type: 'SDK.ConcordiumGRPCClient', - }, - { - docs: ['Generic smart contract module used internally.'], - scope: tsm.Scope.Private, - isReadonly: true, - name: deployedModuleId, - type: 'SDK.DeployedModule', + name: internalModuleClientId, + type: 'SDK.ModuleClient.Type', }, ], }); - moduleClassDecl.addConstructor({ + + moduleClassDecl + .addConstructor({ + docs: [ + 'Constructor is only ment to be used internally in this module. Use functions such as `create` or `createUnchecked` for construction.', + ], + parameters: [ + { + name: internalModuleClientId, + type: 'SDK.ModuleClient.Type', + }, + ], + }) + .setBodyText( + `this.${internalModuleClientId} = ${internalModuleClientId};` + ); + + moduleSourceFile.addTypeAlias({ docs: [ - 'Private constructor to enforce creating objects using a static method.', + `Client for an on-chain smart contract module with module reference '${moduleRef.moduleRef}', can be used for instantiating new smart contract instances.`, ], - scope: tsm.Scope.Private, - parameters: [ - { - name: grpcClientId, - type: 'SDK.ConcordiumGRPCClient', - }, - { - name: deployedModuleId, - type: 'SDK.DeployedModule', - }, - ], - }).setBodyText(`this.${grpcClientId} = ${grpcClientId}; -this.${deployedModuleId} = ${deployedModuleId};`); + name: 'Type', + isExported: true, + type: moduleClientType, + }); - moduleClassDecl - .addMethod({ + const grpcClientId = 'grpcClient'; + moduleSourceFile + .addFunction({ docs: [ - `Construct the \`${moduleClientId}\` for interacting with a module on chain. -Checks and throws an error if the module is not deployed on chain. + `Construct a ${moduleClientType} client for interacting with a smart contract module on chain. +This function ensures the smart contract module is deployed on chain. -@param {ConcordiumGRPCClient} ${grpcClientId} - The GRPC client for accessing a node. -@param {ModuleReference} ${moduleRefId} - The reference of the deployed smart contract module. +@param {SDK.ConcordiumGRPCClient} ${grpcClientId} - The concordium node client to use. -@throws If failing to communicate with the concordium node or module reference does not correspond to a module on chain. +@throws If failing to communicate with the concordium node or if the module reference is not present on chain. -@returns {${moduleClientId}}`, +@returns {${moduleClientType}} A module client ensured to be deployed on chain.`, ], - scope: tsm.Scope.Public, - isStatic: true, + isExported: true, isAsync: true, name: 'create', parameters: [ @@ -194,29 +200,25 @@ Checks and throws an error if the module is not deployed on chain. type: 'SDK.ConcordiumGRPCClient', }, ], - returnType: `Promise<${moduleClientId}>`, + returnType: `Promise<${moduleClientType}>`, }) .setBodyText( - `return new ${moduleClientId}( - ${grpcClientId}, - await SDK.DeployedModule.create(${grpcClientId}, ${moduleClientId}.${moduleRefId}), -);` + `const moduleClient = await SDK.ModuleClient.create(${grpcClientId}, ${moduleRefId}); +return new ${moduleClientType}(moduleClient);` ); - moduleClassDecl - .addMethod({ + moduleSourceFile + .addFunction({ docs: [ - `Construct the \`${moduleClientId}\` for interacting with a module on chain. -The caller must ensure the module is deployed on chain. + `Construct a ${moduleClientType} client for interacting with a smart contract module on chain. +It is up to the caller to ensure the module is deployed on chain. -@param {ConcordiumGRPCClient} ${grpcClientId} - The GRPC client for accessing a node. -@param {ModuleReference} ${moduleRefId} - The reference of the deployed smart contract module. +@param {SDK.ConcordiumGRPCClient} ${grpcClientId} - The concordium node client to use. -@throws If failing to communicate with the concordium node or module reference does not correspond to a module on chain. +@throws If failing to communicate with the concordium node. -@returns {${moduleClientId}}`, +@returns {${moduleClientType}}`, ], - scope: tsm.Scope.Public, - isStatic: true, + isExported: true, name: 'createUnchecked', parameters: [ { @@ -224,145 +226,170 @@ The caller must ensure the module is deployed on chain. type: 'SDK.ConcordiumGRPCClient', }, ], - returnType: moduleClientId, + returnType: `${moduleClientType}`, }) .setBodyText( - `return new ${moduleClientId}( - ${grpcClientId}, - SDK.DeployedModule.createUnchecked(${grpcClientId}, ${moduleClientId}.${moduleRefId}), -);` + `const moduleClient = SDK.ModuleClient.createUnchecked(${grpcClientId}, ${moduleRefId}); +return new ${moduleClientType}(moduleClient);` ); - const blockHashId = 'blockHash'; - moduleClassDecl - .addMethod({ + const moduleClientId = 'moduleClient'; + + moduleSourceFile + .addFunction({ docs: [ - `Check if this module is deployed to the chain. + `Construct a ${moduleClientType} client for interacting with a smart contract module on chain. +This function ensures the smart contract module is deployed on chain. -@param {string} [${blockHashId}] Hash of the block to check information at. When not provided the last finalized block is used. +@param {${moduleClientType}} ${moduleClientId} - The client of the on-chain smart contract module with referecence '${moduleRef.moduleRef}'. +@throws If failing to communicate with the concordium node or if the module reference is not present on chain. -@throws {RpcError} If failing to communicate with the concordium node.`, +@returns {${moduleClientType}} A module client ensured to be deployed on chain.`, ], - scope: tsm.Scope.Public, + isExported: true, name: 'checkOnChain', parameters: [ { - name: blockHashId, - hasQuestionToken: true, - type: 'string', + name: moduleClientId, + type: moduleClientType, }, ], returnType: 'Promise', }) .setBodyText( - `return this.${deployedModuleId}.checkOnChain(${blockHashId});` + `return SDK.ModuleClient.checkOnChain(${moduleClientId}.${internalModuleClientId});` ); - moduleClassDecl - .addMethod({ + moduleSourceFile + .addFunction({ docs: [ `Get the module source of the deployed smart contract module. -@param {string} [${blockHashId}] Hash of the block to check information at. When not provided the last finalized block is used. - -@throws {RpcError} If failing to communicate with the concordium node or module not found. - +@param {${moduleClientType}} ${moduleClientId} - The client of the on-chain smart contract module with referecence '${moduleRef.moduleRef}'. +@throws {SDK.RpcError} If failing to communicate with the concordium node or module not found. @returns {SDK.VersionedModuleSource} Module source of the deployed smart contract module.`, ], - scope: tsm.Scope.Public, + isExported: true, name: 'getModuleSource', parameters: [ { - name: blockHashId, - hasQuestionToken: true, - type: 'string', + name: moduleClientId, + type: moduleClientType, }, ], returnType: 'Promise', }) .setBodyText( - `return this.${deployedModuleId}.getModuleSource(${blockHashId});` + `return SDK.ModuleClient.getModuleSource(${moduleClientId}.${internalModuleClientId});` ); - for (const contract of moduleInterface.values()) { - const contractNameId = 'contractName'; - const genericContractId = 'genericContract'; - const contractAddressId = 'contractAddress'; - const dryRunId = 'dryRun'; - const contractClassId = toPascalCase(contract.contractName); - const contractDryRunClassId = `${contractClassId}DryRun`; - const initContractId = `init${contractClassId}`; - - const transactionMetadataId = 'transactionMetadata'; - const parameterId = 'parameter'; - const signerId = 'signer'; + const transactionMetadataId = 'transactionMetadata'; + const parameterId = 'parameter'; + const signerId = 'signer'; - moduleClassDecl - .addMethod({ + for (const contract of moduleInterface.values()) { + moduleSourceFile + .addFunction({ docs: [ `Send transaction for instantiating a new '${contract.contractName}' smart contract instance. -@param {SDK.ContractTransactionMetadata} ${transactionMetadataId} - Metadata to be used for the transaction (with defaults). -@param {SDK.HexString} ${parameterId} - Input for for contract function. -@param {SDK.AccountSigner} ${signerId} - An object to use for signing the transaction. +@param {${moduleClientType}} ${moduleClientId} - The client of the on-chain smart contract module with referecence '${moduleRef.moduleRef}'. +@param {SDK.ContractTransactionMetadata} ${transactionMetadataId} - Metadata related to constructing a transaction for a smart contract module. +@param {SDK.Parameter.Type} ${parameterId} - Parameter to provide as part of the transaction for the instantiation of a new smart contract contract. +@param {SDK.AccountSigner} ${signerId} - The signer of the update contract transaction. -@throws If the query could not be invoked successfully. +@throws If failing to communicate with the concordium node. -@returns {SDK.HexString} The transaction hash of the update transaction.`, +@returns {SDK.TransactionHash.Type}`, ], - scope: tsm.Scope.Public, - name: initContractId, + isExported: true, + name: `instantiate${toPascalCase(contract.contractName)}`, parameters: [ + { + name: moduleClientId, + type: moduleClientType, + }, { name: transactionMetadataId, type: 'SDK.ContractTransactionMetadata', }, { name: parameterId, - type: 'SDK.HexString', + type: 'SDK.Parameter.Type', }, { name: signerId, type: 'SDK.AccountSigner', }, ], - returnType: 'Promise', + returnType: 'Promise', }) .setBodyText( - `return this.${deployedModuleId}.createAndSendInitTransaction( + `return SDK.ModuleClient.createAndSendInitTransaction( + ${moduleClientId}.${internalModuleClientId}, '${contract.contractName}', - SDK.encodeHexString, ${transactionMetadataId}, ${parameterId}, ${signerId} );` ); - const contractClassDecl = sourceFile.addClass({ - docs: ['Smart contract client for a contract instance on chain.'], + const contractOutputFilePath = path.format({ + dir: outDirPath, + name: contract.contractName, + ext: '.ts', + }); + const contractSourceFile = project.createSourceFile( + contractOutputFilePath, + '', + { + overwrite: true, + } + ); + + const moduleRefId = 'moduleReference'; + const grpcClientId = 'grpcClient'; + const contractNameId = 'contractName'; + const genericContractId = 'genericContract'; + const contractAddressId = 'contractAddress'; + const blockHashId = 'blockHash'; + const contractClientType = `${toPascalCase( + contract.contractName + )}Contract`; + + contractSourceFile.addImportDeclaration({ + namespaceImport: 'SDK', + moduleSpecifier: '@concordium/common-sdk', + }); + contractSourceFile.addImportDeclaration({ + namedImports: [moduleRefId], + moduleSpecifier: `./${outModuleName}`, + }); + + contractSourceFile.addVariableStatement({ isExported: true, - name: contractClassId, - properties: [ + declarationKind: tsm.VariableDeclarationKind.Const, + docs: ['Name of the smart contract supported by this client.'], + declarations: [ { - docs: [ - 'The reference of the module used by this contract.', - ], - isStatic: true, - isReadonly: true, - scope: tsm.Scope.Public, - name: moduleRefId, - initializer: moduleRefId, + name: contractNameId, + type: 'SDK.ContractName.Type', + initializer: `SDK.ContractName.fromStringUnchecked('${contract.contractName}')`, }, + ], + }); + + const contractClassDecl = contractSourceFile.addClass({ + docs: ['Smart contract client for a contract instance on chain.'], + name: contractClientType, + properties: [ { docs: [ - 'Name of the smart contract supported by this client.', + 'Having a private field prevents similar structured objects to be considered the same type (similar to nominal typing).', ], - scope: tsm.Scope.Public, - isStatic: true, - isReadonly: true, - name: contractNameId, - type: 'string', - initializer: `'${contract.contractName}'`, + scope: tsm.Scope.Private, + name: '__nominal', + initializer: 'true', }, { docs: ['The gRPC connection used by this client.'], @@ -378,16 +405,10 @@ The caller must ensure the module is deployed on chain. name: contractAddressId, type: 'SDK.ContractAddress', }, - { - docs: ['Dry run entrypoints of the smart contract.'], - scope: tsm.Scope.Public, - isReadonly: true, - name: dryRunId, - type: contractDryRunClassId, - }, + { docs: ['Generic contract client used internally.'], - scope: tsm.Scope.Private, + scope: tsm.Scope.Public, isReadonly: true, name: genericContractId, type: 'SDK.Contract', @@ -395,49 +416,31 @@ The caller must ensure the module is deployed on chain. ], }); - 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, - }); - contractClassDecl .addConstructor({ - docs: [ - 'Private constructor to enforce creating objects using a static method.', - ], - scope: tsm.Scope.Private, parameters: [ - { - name: grpcClientId, - type: 'SDK.ConcordiumGRPCClient', - }, - { - name: contractAddressId, - type: 'SDK.ContractAddress', - }, - { - name: genericContractId, - type: 'SDK.Contract', - }, - { - name: dryRunId, - type: contractDryRunClassId, - }, + { name: grpcClientId, type: 'SDK.ConcordiumGRPCClient' }, + { name: contractAddressId, type: 'SDK.ContractAddress' }, + { name: genericContractId, type: 'SDK.Contract' }, ], }) .setBodyText( - `this.${grpcClientId} = ${grpcClientId}; -this.${contractAddressId} = ${contractAddressId}; -this.${genericContractId} = ${genericContractId}; -this.${dryRunId} = ${dryRunId};` + [grpcClientId, contractAddressId, genericContractId] + .map((name) => `this.${name} = ${name};`) + .join('\n') ); - contractClassDecl - .addMethod({ + + contractSourceFile.addTypeAlias({ + docs: ['Smart contract client for a contract instance on chain.'], + name: 'Type', + isExported: true, + type: contractClientType, + }); + + contractSourceFile + .addFunction({ docs: [ - `Construct an instance of \`${contractClassId}\` for interacting with a '${contract.contractName}' contract on chain. + `Construct an instance of \`${contractClientType}\` for interacting with a '${contract.contractName}' contract on chain. Checking the information instance on chain. @param {SDK.ConcordiumGRPCClient} ${grpcClientId} - The client used for contract invocations and updates. @@ -446,12 +449,10 @@ Checking the information instance on chain. @throws If failing to communicate with the concordium node or if any of the checks fails. -@returns {${contractClassId}} -`, +@returns {${contractClientType}}`, ], - isStatic: true, + isExported: true, isAsync: true, - scope: tsm.Scope.Public, name: 'create', parameters: [ { @@ -465,36 +466,33 @@ Checking the information instance on chain. { name: blockHashId, hasQuestionToken: true, - type: 'string', + type: 'SDK.BlockHash.Type', }, ], - returnType: `Promise<${contractClassId}>`, + returnType: `Promise<${contractClientType}>`, }) .setBodyText( - `const ${genericContractId} = new SDK.Contract(${grpcClientId}, ${contractAddressId}, ${contractClassId}.${contractNameId}); -await ${genericContractId}.checkOnChain({ moduleReference: ${moduleRefId}, blockHash: ${blockHashId} }); -return new ${contractClassId}( + `const ${genericContractId} = new SDK.Contract(${grpcClientId}, ${contractAddressId}, SDK.ContractName.toString(${contractNameId})); +await ${genericContractId}.checkOnChain({ moduleReference: ${moduleRefId}, blockHash: ${blockHashId} === undefined ? undefined : SDK.BlockHash.toHexString(${blockHashId}) }); +return new ${contractClientType}( ${grpcClientId}, ${contractAddressId}, - ${genericContractId}, - new ${contractDryRunClassId}(${genericContractId}) + ${genericContractId} );` ); - contractClassDecl - .addMethod({ + contractSourceFile + .addFunction({ docs: [ - `Construct the \`${contractClassId}\` for interacting with a '${contract.contractName}' contract on chain. + `Construct the \`${contractClientType}\` for interacting with a '${contract.contractName}' contract on chain. Without checking the instance information on chain. @param {SDK.ConcordiumGRPCClient} ${grpcClientId} - The client used for contract invocations and updates. @param {SDK.ContractAddress} ${contractAddressId} - Address of the contract instance. -@returns {${contractClassId}} -`, +@returns {${contractClientType}}`, ], - isStatic: true, - scope: tsm.Scope.Public, + isExported: true, name: 'createUnchecked', parameters: [ { @@ -506,108 +504,140 @@ Without checking the instance information on chain. type: 'SDK.ContractAddress', }, ], - returnType: contractClassId, + returnType: contractClientType, }) .setBodyText( - `const ${genericContractId} = new SDK.Contract(${grpcClientId}, ${contractAddressId}, ${contractClassId}.${contractNameId}); -return new ${contractClassId}( - ${grpcClientId}, - ${contractAddressId}, - ${genericContractId}, - new ${contractDryRunClassId}(${genericContractId}) -);` + `const ${genericContractId} = new SDK.Contract(${grpcClientId}, ${contractAddressId}, SDK.ContractName.toString(${contractNameId})); + return new ${contractClientType}( + ${grpcClientId}, + ${contractAddressId}, + ${genericContractId}, + );` ); - dryRunClassDecl.addConstructor({ - docs: ['Contruct a client for a contract instance on chain'], - parameters: [ - { - name: genericContractId, - type: 'SDK.Contract', - scope: tsm.Scope.Private, - }, - ], - }); + const contractClientId = 'contractClient'; + contractSourceFile + .addFunction({ + docs: [ + `Check if the smart contract instance exists on the blockchain and whether it uses a matching contract name and module reference. + +@param {${contractClientType}} ${contractClientId} The client for a '${contract.contractName}' smart contract instance on chain. +@param {SDK.BlockHash.Type} [${blockHashId}] A optional block hash to use for checking information on chain, if not provided the last finalized will be used. + +@throws {SDK.RpcError} If failing to communicate with the concordium node or if any of the checks fails.`, + ], + isExported: true, + name: 'checkOnChain', + parameters: [ + { + name: contractClientId, + type: contractClientType, + }, + { + name: blockHashId, + hasQuestionToken: true, + type: 'SDK.BlockHash.Type', + }, + ], + returnType: 'Promise', + }) + .setBodyText( + `return ${contractClientId}.${genericContractId}.checkOnChain({moduleReference: ${moduleRefId}, blockHash: ${blockHashId} === undefined ? undefined : SDK.BlockHash.toHexString(${blockHashId})})` + ); + + const invokerId = 'invoker'; for (const entrypointName of contract.entrypointNames) { - const transactionMetadataId = 'transactionMetadata'; - const parameterId = 'parameter'; - const signerId = 'signer'; - contractClassDecl - .addMethod({ + contractSourceFile + .addFunction({ docs: [ `Send an update-contract transaction to the '${entrypointName}' entrypoint of the '${contract.contractName}' contract. -@param {SDK.ContractTransactionMetadata} ${transactionMetadataId} - Hex encoded parameter for entrypoint -@param {SDK.HexString} ${parameterId} - Hex encoded parameter for entrypoint +@param {${contractClientType}} ${contractClientId} The client for a '${contract.contractName}' smart contract instance on chain. +@param {SDK.ContractTransactionMetadata} ${transactionMetadataId} - Metadata related to constructing a transaction for a smart contract. +@param {SDK.Parameter.Type} ${parameterId} - Parameter to provide the smart contract entrypoint as part of the transaction. @param {SDK.AccountSigner} ${signerId} - The signer of the update contract transaction. @throws If the entrypoint is not successfully invoked. -@returns {SDK.HexString} Transaction hash`, +@returns {SDK.TransactionHash.Type} Transaction hash`, ], - scope: tsm.Scope.Public, - name: toCamelCase(entrypointName), + isExported: true, + name: `send${toPascalCase(entrypointName)}`, parameters: [ + { + name: contractClientId, + type: contractClientType, + }, { name: transactionMetadataId, type: 'SDK.ContractTransactionMetadata', }, { name: parameterId, - type: 'SDK.HexString', + type: 'SDK.Parameter.Type', }, { name: signerId, type: 'SDK.AccountSigner', }, ], - returnType: 'Promise', + returnType: 'Promise', }) .setBodyText( - `return this.${genericContractId}.createAndSendUpdateTransaction( + `return ${contractClientId}.${genericContractId}.createAndSendUpdateTransaction( '${entrypointName}', SDK.encodeHexString, ${transactionMetadataId}, - ${parameterId}, + SDK.Parameter.toHexString(${parameterId}), ${signerId} -);` +).then(SDK.TransactionHash.fromHexString);` ); - const blockHashId = 'blockHash'; - dryRunClassDecl - .addMethod({ + + contractSourceFile + .addFunction({ docs: [ - `Dry run an update-contract transaction to the '${entrypointName}' entrypoint of the '${contract.contractName}' contract. + `Dry-run an update-contract transaction to the '${entrypointName}' entrypoint of the '${contract.contractName}' contract. -@param {SDK.HexString} ${parameterId} - Hex encoded parameter for entrypoint -@param {SDK.HexString} [${blockHashId}] - Block hash of the block to invoke entrypoint at +@param {${contractClientType}} ${contractClientId} The client for a '${contract.contractName}' smart contract instance on chain. +@param {SDK.ContractAddress | SDK.AccountAddress} ${invokerId} - The address of the account or contract which is invoking this transaction. +@param {SDK.Parameter.Type} ${parameterId} - Parameter to include in the transaction for the smart contract entrypoint. +@param {SDK.BlockHash.Type} [${blockHashId}] - Optional block hash allowing for dry-running the transaction at the end of a specific block. -@throws If the entrypoint is not successfully invoked. +@throws {SDK.RpcError} If failing to communicate with the concordium node or if any of the checks fails. -returns {SDK.HexString} Hex encoded response`, +@returns {SDK.InvokeContractResult} The result of invoking the smart contract instance.`, ], - scope: tsm.Scope.Public, - name: toCamelCase(entrypointName), + isExported: true, + name: `dryRun${toPascalCase(entrypointName)}`, parameters: [ + { + name: contractClientId, + type: contractClientType, + }, + { + name: invokerId, + type: 'SDK.ContractAddress | SDK.AccountAddress', + }, { name: parameterId, - type: 'SDK.HexString', + type: 'SDK.Parameter.Type', }, { name: blockHashId, - type: 'SDK.HexString', hasQuestionToken: true, + type: 'SDK.BlockHash.Type', }, ], - returnType: 'Promise', + returnType: 'Promise', }) .setBodyText( - `return this.${genericContractId}.invokeView( + `return ${contractClientId}.${genericContractId}.dryRun.invokeMethod( '${entrypointName}', + ${invokerId}, SDK.encodeHexString, - (hex: SDK.HexString) => hex, - ${parameterId}, - ${blockHashId} + SDK.Parameter.toHexString(${parameterId}), + ${blockHashId} === undefined ? undefined : SDK.BlockHash.toHexString(${blockHashId}) );` ); } @@ -619,17 +649,6 @@ function capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.substring(1); } -/** - * Convert a string in snake_case or kebab-case into camelCase. - * This is used to transform entrypoint names in the smart contract to follow formatting javascript convention. - */ -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. * This is used to transform contract names in the smart contract to follow formatting javascript convention. diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index 826f0db8e..dbad2b4f2 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -6,9 +6,19 @@ - `sendUpdateInstruction` to the gRPC Client. - `healthCheck` to the gRPC Client. -- Functions `calculateModuleReference` for getting the module reference and `parseModuleInterface` for getting the interface from the source of a smart contract module. +- Function `calculateModuleReference` for getting the module reference. +- Function `parseModuleInterface` for getting the interface from the source of a smart contract module. +- Function `getEmbeddedModuleSchema` for getting the module schema embedded into a smart contract module source. - Smart contract related types `ContractName`, `EntrypointName` and helper functions `isInitName`, `isReceiveName`, `getContractNameFromInit` and `getNamesFromReceive`. -- Add `DeployedModule` class for interaction with a smart contract module deployed on chain. +- Add `ModuleClient` module and type for interaction with a smart contract module deployed on chain. +- Add `Energy` module with helpers for transaction energy. +- Add `BlockHash` module with helpers for block hashes. +- Add `TransactionHash` module with helpers for transaction hashes. +- Add `InitName` module with helpers for smart contract init-function names. +- Add `ContractName` module with helpers for smart contract names. +- Add `Parameter` module with helpers for smart contract parameters. +- Add `AccountSequenceNumber` module with helpers for account sequence numbers (formerly referred to as nonce). +- Add methods `getInstanceInfo` and `checkOnChain` on the generic contract client `Contract`. ### Fixed - Added missing fields to `getBlockChainParameters` response. (rootKeys, level1Keys, level2Keys) diff --git a/packages/common/src/AccountSequenceNumber.ts b/packages/common/src/AccountSequenceNumber.ts new file mode 100644 index 000000000..b8cc47630 --- /dev/null +++ b/packages/common/src/AccountSequenceNumber.ts @@ -0,0 +1,23 @@ +/** Account transaction sequence number. (Formerly refered as Nonce) */ +class AccountSequenceNumber { + /** Having a private field prevents similar structured objects to be considered the same type (similar to nominal typing). */ + private __nominal = true; + constructor(public value: bigint) {} +} + +/** Account transaction sequence number. (Formerly refered as Nonce) */ +export type Type = AccountSequenceNumber; + +/** + * Construct an AccountSequenceNumber type. + * @param {bigint | number} sequenceNumber The account sequence number. + * @returns {AccountSequenceNumber} + */ +export function create(sequenceNumber: bigint | number): AccountSequenceNumber { + if (sequenceNumber < 1) { + throw new Error( + 'Invalid account sequence number: Must be 1 or higher.' + ); + } + return new AccountSequenceNumber(BigInt(sequenceNumber)); +} diff --git a/packages/common/src/BlockHash.ts b/packages/common/src/BlockHash.ts new file mode 100644 index 000000000..728d2a4b8 --- /dev/null +++ b/packages/common/src/BlockHash.ts @@ -0,0 +1,54 @@ +import { HexString } from '.'; + +/** + * Represents a hash of a block in the chain. + */ +class BlockHash { + /** Having a private field prevents similar structured objects to be considered the same type (similar to nominal typing). */ + private __nominal = true; + constructor( + /** The internal buffer of bytes representing the hash. */ + public buffer: ArrayBuffer + ) {} +} + +/** + * Represents a hash of a block in the chain. + */ +export type Type = BlockHash; + +/** + * Create a BlockHash from a buffer of 32 bytes. + * @param {ArrayBuffer} buffer Buffer containing 32 bytes for the hash. + * @throws If the provided buffer does not contain exactly 32 bytes. + * @returns {BlockHash} + */ +export function fromBuffer(buffer: ArrayBuffer): BlockHash { + if (buffer.byteLength !== 32) { + throw new Error( + `Invalid transaction hash provided: Expected a buffer containing 32 bytes, instead got '${Buffer.from( + buffer + ).toString('hex')}'.` + ); + } + return new BlockHash(buffer); +} + +/** + * Create a BlockHash from a hex string. + * @param {HexString} hex Hex encoding of block hash. + * @throws If the provided hex encoding does not correspond to a buffer of exactly 32 bytes. + * @returns {BlockHash} + */ +export function fromHexString(hex: HexString): BlockHash { + return fromBuffer(Buffer.from(hex, 'hex')); +} + +/** + * Hex encode a BlockHash. + * @param {BlockHash} hash The block hash to encode. + * @returns {HexString} String containing the hex encoding. + */ +export function toHexString(hash: BlockHash): HexString { + return Buffer.from(hash.buffer).toString('hex'); +} diff --git a/packages/common/src/ContractName.ts b/packages/common/src/ContractName.ts new file mode 100644 index 000000000..dfe9cada0 --- /dev/null +++ b/packages/common/src/ContractName.ts @@ -0,0 +1,68 @@ +import * as InitName from './InitName'; +import { isAsciiAlphaNumericPunctuation } from './contractHelpers'; + +/** The name of a smart contract. Note: This does _not_ including the 'init_' prefix. */ +class ContractName { + /** Having a private field prevents similar structured objects to be considered the same type (similar to nominal typing). */ + private __nominal = true; + constructor( + /** The internal string value of the contract name. */ + public value: string + ) {} +} + +/** The name of a smart contract. Note: This does _not_ including the 'init_' prefix. */ +export type Type = ContractName; + +/** + * Create a contract name from a string, ensuring it follows the format of a contract name. + * @param {string} value The string of the contract name. + * @throws If the provided value is not a valid contract name. + * @returns {ContractName} + */ +export function fromString(value: string): ContractName { + if (value.length <= 95) { + throw new Error( + 'Invalid ContractName: Can be atmost 95 characters long.' + ); + } + if (value.includes('.')) { + throw new Error( + "Invalid ContractName: Must not contain a '.' character." + ); + } + if (!isAsciiAlphaNumericPunctuation(value)) { + throw new Error( + 'Invalid ContractName: Must only contain ASCII alpha, numeric and punctuation characters.' + ); + } + return new ContractName(value); +} + +/** + * Create a contract name from a string, but _without_ ensuring it follows the format of a contract name. + * It is up to the caller to validate the string is a contract name. + * @param {string} value The string of the contract name. + * @returns {ContractName} + */ +export function fromStringUnchecked(value: string): ContractName { + return new ContractName(value); +} + +/** + * Extract the contract name from an {@link InitName.Type}. + * @param {InitName.Type} initName The init-function name of a smart contract. + * @returns {ContractName} + */ +export function fromInitName(initName: InitName.Type): ContractName { + return fromStringUnchecked(initName.value.substring(5)); +} + +/** + * Convert a contract name to a string + * @param {ContractName} contractName The contract name to stringify. + * @returns {string} + */ +export function toString(contractName: ContractName): string { + return contractName.value; +} diff --git a/packages/common/src/DeployedModule.ts b/packages/common/src/DeployedModule.ts deleted file mode 100644 index 460ef4422..000000000 --- a/packages/common/src/DeployedModule.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { - AccountAddress, - AccountSigner, - AccountTransactionType, - CcdAmount, - ConcordiumGRPCClient, - ContractTransactionMetadata, - HexString, - InitContractPayload, - TransactionExpiry, - VersionedModuleSource, - getContractUpdateDefaultExpiryDate, - signTransaction, -} from '.'; -import { checkParameterLength } from './contractHelpers'; -import { ModuleReference } from './types/moduleReference'; -import { Buffer } from 'buffer/'; - -/** - * An update transaction without header. - */ -export type ContractInitTransaction = { - /** The type of the transaction, which will always be of type {@link AccountTransactionType.InitContract} */ - type: AccountTransactionType.InitContract; - /** The payload of the transaction, which will always be of type {@link InitContractPayload} */ - payload: InitContractPayload; -}; - -/** - * Type representing a smart contract module deployed on chain. - * - * @template C - Union of contract names in the smart contract module. - */ -export class DeployedModule { - /** Private constructor to enforce creating objects using a static method. */ - private constructor( - /** The gRPC connection used by this object */ - public grpcClient: ConcordiumGRPCClient, - /** The reference for this module */ - public moduleReference: ModuleReference - ) {} - - /** - * Create a new `GenericModule` instance for interacting with a smart contract module on chain. - * This function ensures the module is already deployed on chain otherwise produces an error. - * - * @template C - Union of contract names in the smart contract module. - * - * @param {ConcordiumGRPCClient} grpcClient - The GRPC client for accessing a node. - * @param {ModuleReference} moduleReference - The reference of the deployed smart contract module. - * - * @throws If failing to communicate with the concordium node or module reference does not correspond to a module on chain. - * - * @returns {DeployedModule} - */ - public static async create( - grpcClient: ConcordiumGRPCClient, - moduleReference: ModuleReference - ): Promise> { - const mod = new DeployedModule(grpcClient, moduleReference); - await mod.checkOnChain(); - return mod; - } - - /** - * Create a new `GenericModule` instance for interacting with a smart contract module on chain. - * The caller must ensure that the smart contract module is already deployed on chain. - * - * @template C - Union of contract names in the smart contract module. - * - * @param {ConcordiumGRPCClient} grpcClient - The GRPC client for accessing a node. - * @param {ModuleReference} moduleReference - The reference of the deployed smart contract module. - * - * @returns {DeployedModule} - */ - public static createUnchecked( - grpcClient: ConcordiumGRPCClient, - moduleReference: ModuleReference - ): DeployedModule { - return new DeployedModule(grpcClient, moduleReference); - } - - /** - * Check if this module is deployed to the chain. - * - * @param {string} [blockHash] Hash of the block to check information at. When not provided the last finalized block is used. - * - * @throws {RpcError} If failing to communicate with the concordium node or module is not deployed on chain. - * @returns {boolean} Indicating whether the module is deployed on chain. - */ - public async checkOnChain(blockHash?: string): Promise { - await this.getModuleSource(blockHash); - } - - /** - * Get the module source of the deployed smart contract module. - * - * @param {string} [blockHash] Hash of the block to check information at. When not provided the last finalized block is used. - * - * @throws {RpcError} If failing to communicate with the concordium node or module not found. - * @returns {VersionedModuleSource} Module source of the deployed smart contract module. - */ - public getModuleSource(blockHash?: string): Promise { - return this.grpcClient.getModuleSource(this.moduleReference, blockHash); - } - - /** - * Creates and sends transaction for initializing a smart contract `contractName` with parameter `input`. - * - * @template T - The type of the input. - * - * @param {string} contractName - The name of the smart contract to instantiate (this is without the `init_` prefix). - * @param {Function} serializeInput - A function to serialize the `input` to bytes. - * @param {ContractTransactionMetadata} metadata - Metadata to be used for the transaction (with defaults). - * @param {T} input - Input for for contract function. - * @param {AccountSigner} signer - An object to use for signing the transaction. - * - * @throws If the query could not be invoked successfully. - * - * @returns {HexString} The transaction hash of the update transaction - */ - public async createAndSendInitTransaction( - contractName: C, - serializeInput: (input: T) => Buffer, - metadata: ContractTransactionMetadata, - input: T, - signer: AccountSigner - ): Promise { - const parameter = serializeInput(input); - checkParameterLength(parameter); - const payload: InitContractPayload = { - moduleRef: this.moduleReference, - amount: new CcdAmount(metadata.amount ?? 0n), - initName: `init_${contractName}`, - maxContractExecutionEnergy: metadata.energy, - param: parameter, - }; - const sender = new AccountAddress(metadata.senderAddress); - const { nonce } = await this.grpcClient.getNextAccountNonce(sender); - const header = { - expiry: new TransactionExpiry( - metadata.expiry ?? getContractUpdateDefaultExpiryDate() - ), - nonce: nonce, - sender, - }; - const transaction = { - type: AccountTransactionType.InitContract, - header, - payload, - }; - const signature = await signTransaction(transaction, signer); - return this.grpcClient.sendAccountTransaction(transaction, signature); - } -} diff --git a/packages/common/src/Energy.ts b/packages/common/src/Energy.ts new file mode 100644 index 000000000..3cd50ddc7 --- /dev/null +++ b/packages/common/src/Energy.ts @@ -0,0 +1,27 @@ +/** Energy measure. Used as part of cost calculations for transactions. */ +class Energy { + /** Having a private field prevents similar structured objects to be considered the same type (similar to nominal typing). */ + private __nominal = true; + constructor( + /** The internal value for representing the energy. */ + public value: bigint + ) {} +} + +/** Energy measure. Used as part of cost calculations for transactions. */ +export type Type = Energy; + +/** + * Construct an Energy type. + * @param {bigint | number} value The measure of energy. + * @throws If the provided value is a negative number. + * @returns {Energy} + */ +export function create(value: bigint | number): Energy { + if (value < 0) { + throw new Error( + 'Invalid energy: The value cannot be a negative number.' + ); + } + return new Energy(BigInt(value)); +} diff --git a/packages/common/src/GenericContract.ts b/packages/common/src/GenericContract.ts index 68d81d0b8..1422bd38e 100644 --- a/packages/common/src/GenericContract.ts +++ b/packages/common/src/GenericContract.ts @@ -25,6 +25,7 @@ import { AccountAddress } from './types/accountAddress'; import { CcdAmount } from './types/ccdAmount'; import { TransactionExpiry } from './types/transactionExpiry'; import { ModuleReference } from './types/moduleReference'; +import * as BlockHash from './BlockHash'; /** * Metadata necessary for smart contract transactions @@ -158,7 +159,7 @@ export type ContractCheckOnChainOptions = { * Hash of the block to check the information at. * When not provided the last finalized block is used. */ - blockHash?: string; + blockHash?: BlockHash.Type; /** * The expected module reference to be used by the contract instance. * When not provided no check is done against the module reference. @@ -247,14 +248,23 @@ class ContractBase { /** * Get information on this smart contract instance. * - * @param {string} [blockHash] Hash of the block to check information at. When not provided the last finalized block is used. + * @param {BlockHash.Type} [blockHash] Hash of the block to check information at. When not provided the last finalized block is used. * @throws if the {@link InstanceInfo} of the contract could not be found. * @returns {InstanceInfo} The instance info. */ - public async getInstanceInfo(blockHash?: string): Promise { - return this.grpcClient.getInstanceInfo(this.contractAddress, blockHash); + public async getInstanceInfo( + blockHash?: BlockHash.Type + ): Promise { + const blockHashHex = + blockHash === undefined + ? undefined + : BlockHash.toHexString(blockHash); + return this.grpcClient.getInstanceInfo( + this.contractAddress, + blockHashHex + ); } /** diff --git a/packages/common/src/InitName.ts b/packages/common/src/InitName.ts new file mode 100644 index 000000000..25fe9417f --- /dev/null +++ b/packages/common/src/InitName.ts @@ -0,0 +1,58 @@ +import * as ContractName from './ContractName'; +import { isAsciiAlphaNumericPunctuation } from './contractHelpers'; + +/** The name of an init-function for a smart contract. Note: This is of the form 'init_'. */ +class InitName { + /** Having a private field prevents similar structured objects to be considered the same type (similar to nominal typing). */ + private __nominal = true; + constructor( + /** The internal string corresponding to the init-function. */ + public value: string + ) {} +} + +/** The name of an init-function for a smart contract. Note: This is of the form 'init_'. */ +export type Type = InitName; + +/** + * Create an InitName directly from a string, ensuring it follows the format of an init-function name. + * @param {string} value String with the init-function name. + * @throws If the string is not a valid init-function name. + * @returns {InitName} + */ +export function fromString(value: string): InitName { + if (value.length <= 100) { + throw new Error('Invalid InitName: Can be atmost 100 characters long.'); + } + if (value.startsWith('init_')) { + throw new Error("Invalid InitName: Must be prefixed with 'init_'."); + } + if (value.includes('.')) { + throw new Error("Invalid InitName: Must not contain a '.' character."); + } + if (!isAsciiAlphaNumericPunctuation(value)) { + throw new Error( + 'Invalid InitName: Must only contain ASCII alpha, numeric and punctuation characters.' + ); + } + return new InitName(value); +} + +/** + * Create an InitName directly from a string. + * It is up to the caller to ensure the provided string follows the format of an init-function name. + * @param {string} value String with the init-function name. + * @returns {InitName} + */ +export function fromStringUnchecked(value: string): InitName { + return new InitName(value); +} + +/** + * Create an InitName from a contract name. + * @param {ContractName.Type} contractName The contract name to convert into an init-function name. + * @returns {InitName} + */ +export function fromContractName(contractName: ContractName.Type): InitName { + return fromStringUnchecked('init_' + contractName.value); +} diff --git a/packages/common/src/ModuleClient.ts b/packages/common/src/ModuleClient.ts new file mode 100644 index 000000000..b3500d216 --- /dev/null +++ b/packages/common/src/ModuleClient.ts @@ -0,0 +1,172 @@ +import { + AccountAddress, + AccountSigner, + AccountTransactionType, + CcdAmount, + ConcordiumGRPCClient, + ContractTransactionMetadata, + InitContractPayload, + TransactionExpiry, + VersionedModuleSource, + getContractUpdateDefaultExpiryDate, + signTransaction, +} from '.'; +import { ModuleReference } from './types/moduleReference'; +import { Buffer } from 'buffer/'; +import * as BlockHash from './BlockHash'; +import * as Parameter from './Parameter'; +import * as TransactionHash from './TransactionHash'; +import * as ContractName from './ContractName'; + +/** + * An update transaction without header. + */ +export type ContractInitTransaction = { + /** The type of the transaction, which will always be of type {@link AccountTransactionType.InitContract} */ + type: AccountTransactionType.InitContract; + /** The payload of the transaction, which will always be of type {@link InitContractPayload} */ + payload: InitContractPayload; +}; + +/** + * Internal class representing a smart contract module deployed on chain. + * + * The public type for this {@link ModuleClient} is exported separately to ensure + * the constructor is only available from within this module. + */ +class ModuleClient { + /** Having a private field prevents similar structured objects to be considered the same type (similar to nominal typing). */ + private __nominal = true; + constructor( + /** The gRPC connection used by this object */ + public grpcClient: ConcordiumGRPCClient, + /** The reference for this module */ + public moduleReference: ModuleReference + ) {} +} + +/** + * Type representing a smart contract module deployed on chain. + */ +export type Type = ModuleClient; + +/** + * Create a new `GenericModule` instance for interacting with a smart contract module on chain. + * The caller must ensure that the smart contract module is already deployed on chain. + * + * @param {ConcordiumGRPCClient} grpcClient - The GRPC client for accessing a node. + * @param {ModuleReference} moduleReference - The reference of the deployed smart contract module. + * + * @returns {ModuleClient} + */ +export function createUnchecked( + grpcClient: ConcordiumGRPCClient, + moduleReference: ModuleReference +): ModuleClient { + return new ModuleClient(grpcClient, moduleReference); +} + +/** + * Create a new `GenericModule` instance for interacting with a smart contract module on chain. + * This function ensures the module is already deployed on chain otherwise produces an error. + * + * @param {ConcordiumGRPCClient} grpcClient - The GRPC client for accessing a node. + * @param {ModuleReference} moduleReference - The reference of the deployed smart contract module. + * + * @throws If failing to communicate with the concordium node or module reference does not correspond to a module on chain. + * + * @returns {ModuleClient} + */ +export async function create( + grpcClient: ConcordiumGRPCClient, + moduleReference: ModuleReference +): Promise { + const mod = new ModuleClient(grpcClient, moduleReference); + await checkOnChain(mod); + return mod; +} + +/** + * Check if this module is deployed to the chain. + * + * @param {ModuleClient} moduleClient The client for a smart contract module on chain. + * @param {BlockHash.Type} [blockHash] Hash of the block to check information at. When not provided the last finalized block is used. + * + * @throws {RpcError} If failing to communicate with the concordium node or module is not deployed on chain. + * @returns {boolean} Indicating whether the module is deployed on chain. + */ +export async function checkOnChain( + moduleClient: ModuleClient, + blockHash?: BlockHash.Type +): Promise { + await getModuleSource(moduleClient, blockHash); +} + +/** + * Get the module source of the deployed smart contract module. + * + * @param {ModuleClient} moduleClient The client for a smart contract module on chain. + * @param {BlockHash.Type} [blockHash] Hash of the block to check information at. When not provided the last finalized block is used. + * + * @throws {RpcError} If failing to communicate with the concordium node or module not found. + * @returns {VersionedModuleSource} Module source of the deployed smart contract module. + */ +export function getModuleSource( + moduleClient: ModuleClient, + blockHash?: BlockHash.Type +): Promise { + return moduleClient.grpcClient.getModuleSource( + moduleClient.moduleReference, + blockHash === undefined ? undefined : BlockHash.toHexString(blockHash) + ); +} + +/** + * Creates and sends transaction for initializing a smart contract `contractName` with parameter `input`. + * + * + * @param {ModuleClient} moduleClient The client for a smart contract module on chain. + * @param {ContractName.Type} contractName - The name of the smart contract to instantiate (this is without the `init_` prefix). + * @param {ContractTransactionMetadata} metadata - Metadata to be used for the transaction (with defaults). + * @param {Parameter.Type} parameter - Input for for contract function. + * @param {AccountSigner} signer - An object to use for signing the transaction. + * + * @throws If the query could not be invoked successfully. + * + * @returns {TransactionHash.Type} The transaction hash of the update transaction. + */ +export async function createAndSendInitTransaction( + moduleClient: ModuleClient, + contractName: ContractName.Type, + metadata: ContractTransactionMetadata, + parameter: Parameter.Type, + signer: AccountSigner +): Promise { + const payload: InitContractPayload = { + moduleRef: moduleClient.moduleReference, + amount: new CcdAmount(metadata.amount ?? 0n), + initName: `init_${contractName}`, + maxContractExecutionEnergy: metadata.energy, + param: Buffer.from(parameter.buffer), + }; + const sender = new AccountAddress(metadata.senderAddress); + const { nonce } = await moduleClient.grpcClient.getNextAccountNonce(sender); + const header = { + expiry: new TransactionExpiry( + metadata.expiry ?? getContractUpdateDefaultExpiryDate() + ), + nonce: nonce, + sender, + }; + const transaction = { + type: AccountTransactionType.InitContract, + header, + payload, + }; + const signature = await signTransaction(transaction, signer); + const hash = await moduleClient.grpcClient.sendAccountTransaction( + transaction, + signature + ); + return TransactionHash.fromHexString(hash); +} diff --git a/packages/common/src/Parameter.ts b/packages/common/src/Parameter.ts new file mode 100644 index 000000000..fc9f7d92e --- /dev/null +++ b/packages/common/src/Parameter.ts @@ -0,0 +1,57 @@ +import { checkParameterLength } from './contractHelpers'; +import { HexString } from './types'; + +/** Parameter for a smart contract entrypoint. */ +class Parameter { + /** Having a private field prevents similar structured objects to be considered the same type (similar to nominal typing). */ + private __nominal = true; + constructor( + /** Internal buffer of bytes representing the parameter. */ + public buffer: ArrayBuffer + ) {} +} + +/** Parameter for a smart contract entrypoint. */ +export type Type = Parameter; + +/** + * Create a parameter for a smart contract entrypoint. + * Ensuring the buffer does not exceed the maximum number of bytes supported for a smart contract parameter. + * @param {ArrayBuffer} buffer The buffer of bytes representing the parameter. + * @throws If the provided buffer exceed the supported number of bytes for a smart contract. + * @returns {Parameter} + */ +export function fromBuffer(buffer: ArrayBuffer): Parameter { + checkParameterLength(buffer); + return new Parameter(buffer); +} + +/** + * Create an unchecked parameter for a smart contract entrypoint. + * It is up to the caller to ensure the buffer does not exceed the maximum number of bytes supported for a smart contract parameter. + * @param {ArrayBuffer} buffer The buffer of bytes representing the parameter. + * @returns {Parameter} + */ +export function fromBufferUnchecked(buffer: ArrayBuffer): Parameter { + return new Parameter(buffer); +} + +/** + * Create a parameter for a smart contract entrypoint from a hex string. + * Ensuring the parameter does not exceed the maximum number of bytes supported for a smart contract parameter. + * @param {HexString} hex String with hex encoding of the parameter. + * @throws If the provided parameter exceed the supported number of bytes for a smart contract. + * @returns {Parameter} + */ +export function fromHexString(hex: HexString): Parameter { + return fromBuffer(Buffer.from(hex, 'hex')); +} + +/** + * Convert a parameter into a hex string. + * @param {Parameter} parameter The parameter to encode in a hex string. + * @returns {HexString} + */ +export function toHexString(parameter: Parameter): HexString { + return Buffer.from(parameter.buffer).toString('hex'); +} diff --git a/packages/common/src/TransactionHash.ts b/packages/common/src/TransactionHash.ts new file mode 100644 index 000000000..fb979f980 --- /dev/null +++ b/packages/common/src/TransactionHash.ts @@ -0,0 +1,50 @@ +import { HexString } from '.'; + +/** Hash of a transaction. */ +class TransactionHash { + /** Having a private field prevents similar structured objects to be considered the same type (similar to nominal typing). */ + private __nominal = true; + constructor( + /** Internal buffer with the hash. */ + public buffer: ArrayBuffer + ) {} +} + +/** Hash of a transaction. */ +export type Type = TransactionHash; + +/** + * Create a TransactionHash from a buffer. + * @param {ArrayBuffer} buffer Bytes for the transaction hash. Must be exactly 32 bytes. + * @throws If the provided buffer does not contain 32 bytes. + * @returns {TransactionHash} + */ +export function fromBuffer(buffer: ArrayBuffer): TransactionHash { + if (buffer.byteLength !== 32) { + throw new Error( + `Invalid transaction hash provided: Expected a buffer containing 32 bytes, instead got '${Buffer.from( + buffer + ).toString('hex')}'.` + ); + } + return new TransactionHash(buffer); +} + +/** + * Create a TransactionHash from a hex string. + * @param {HexString} hex String with hex encoding of the transaction hash. + * @throws if the encoding does not correspond to exactly 32 bytes. + * @returns {TransactionHash} + */ +export function fromHexString(hex: HexString): TransactionHash { + return fromBuffer(Buffer.from(hex, 'hex')); +} + +/** + * Convert a transaction hash into a hex encoded string. + * @param {TransactionHash} hash TransactionHash to convert to hex. + * @returns {HexString} String with hex encoding. + */ +export function toHexString(hash: TransactionHash): HexString { + return Buffer.from(hash.buffer).toString('hex'); +} diff --git a/packages/common/src/contractHelpers.ts b/packages/common/src/contractHelpers.ts index da7ed7760..30d09eee1 100644 --- a/packages/common/src/contractHelpers.ts +++ b/packages/common/src/contractHelpers.ts @@ -1,4 +1,3 @@ -import { Buffer } from 'buffer/'; import { ContractAddress, InstanceInfo } from './types'; const CONTRACT_PARAM_MAX_LENGTH = 65535; @@ -25,8 +24,8 @@ export const getContractName = ({ name }: InstanceInfo): string => { * * @throws If buffer exceeds max length allowed for smart contract parameters */ -export const checkParameterLength = (buffer: Buffer): void => { - if (buffer.length > CONTRACT_PARAM_MAX_LENGTH) { +export const checkParameterLength = (buffer: ArrayBuffer): void => { + if (buffer.byteLength > CONTRACT_PARAM_MAX_LENGTH) { throw new Error( `Serialized parameter exceeds max length of smart contract parameter (${CONTRACT_PARAM_MAX_LENGTH} bytes)` ); @@ -43,15 +42,11 @@ export const isEqualContractAddress = /** The name of a smart contract. Note: This does _not_ including the 'init_' prefix. */ export type ContractName = string; -/** The name of a receive function exposed in a smart contract module. Note: This is of the form '.'. */ -export type ReceiveName = string; -/** The name of an init function exposed in a smart contract module. Note: This is of the form 'init_'. */ -export type InitName = string; /** The name of an entrypoint exposed by a smart contract. Note: This does _not_ include the '.' prefix. */ export type EntrypointName = string; /** Check that every character is an Ascii alpha, numeric or punctuation. */ -function isAsciiAlphaNumericPunctuation(string: string) { +export function isAsciiAlphaNumericPunctuation(string: string): boolean { for (let i = 0; i < string.length; i++) { const charCode = string.charCodeAt(i); if ( @@ -72,7 +67,7 @@ function isAsciiAlphaNumericPunctuation(string: string) { } /** Check if a string is a valid smart contract init name. */ -export function isInitName(string: string): string is InitName { +export function isInitName(string: string): boolean { return ( string.length <= 100 && string.startsWith('init_') && @@ -82,12 +77,12 @@ export function isInitName(string: string): string is InitName { } /** Get the contract name from a string. Assumes the string is a valid init name. */ -export function getContractNameFromInit(initName: InitName): ContractName { +export function getContractNameFromInit(initName: string): ContractName { return initName.substring(5); } /** Check if a string is a valid smart contract receive name. */ -export function isReceiveName(string: string): string is ReceiveName { +export function isReceiveName(string: string): boolean { return ( string.length <= 100 && string.includes('.') && @@ -96,7 +91,7 @@ export function isReceiveName(string: string): string is ReceiveName { } /** Get the contract name and entrypoint name from a string. Assumes the string is a valid receive name. */ -export function getNamesFromReceive(receiveName: ReceiveName): { +export function getNamesFromReceive(receiveName: string): { contractName: ContractName; entrypointName: EntrypointName; } { diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 7299961bc..93cc8ce76 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -78,4 +78,12 @@ export * from './cis2'; export * from './cis0'; export * from './cis4'; export * from './GenericContract'; -export * from './DeployedModule'; + +export * as ModuleClient from './ModuleClient'; +export * as Parameter from './Parameter'; +export * as AccountSequenceNumber from './AccountSequenceNumber'; +export * as Energy from './Energy'; +export * as TransactionHash from './TransactionHash'; +export * as BlockHash from './BlockHash'; +export * as ContractName from './ContractName'; +export * as InitName from './InitName'; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 9d41b6fe0..e46b2dc6e 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -16,7 +16,6 @@ import { TransactionEvent, TransferredEvent, } from './types/transactionEvent'; -import { InitName, ReceiveName } from './contractHelpers'; export * from './types/NodeInfo'; export * from './types/PeerInfo'; @@ -1783,9 +1782,9 @@ export interface InstanceInfoCommon { /** Account used to instantiate this smart contract instance. */ owner: AccountAddress; /** List of receive functions currently exposed by this smart contract. These are of the form '.'. */ - methods: ReceiveName[]; + methods: string[]; /** Name of the smart contract. This is of the form 'init_'. */ - name: InitName; + name: string; } export interface InstanceInfoV0 extends InstanceInfoCommon { diff --git a/packages/common/src/types/VersionedModuleSource.ts b/packages/common/src/types/VersionedModuleSource.ts index fcda8bc37..adcc1440b 100644 --- a/packages/common/src/types/VersionedModuleSource.ts +++ b/packages/common/src/types/VersionedModuleSource.ts @@ -100,8 +100,28 @@ export async function getEmbeddedModuleSchema( moduleSource: VersionedModuleSource ): Promise { const wasmModule = await WebAssembly.compile(moduleSource.source); - const buffer = schemaBytesFromWasmModule(wasmModule); - return buffer === null ? null : { type: 'versioned', buffer }; + const versionedSchema = schemaBytesFromWasmModule( + wasmModule, + 'concordium-schema' + ); + if (versionedSchema !== null) { + return { type: 'versioned', buffer: versionedSchema }; + } + const unversionedSchemaV0 = schemaBytesFromWasmModule( + wasmModule, + 'concordium-schema-v1' + ); + if (unversionedSchemaV0 !== null) { + return { type: 'unversioned', version: 0, buffer: unversionedSchemaV0 }; + } + const unversionedSchemaV1 = schemaBytesFromWasmModule( + wasmModule, + 'concordium-schema-v2' + ); + if (unversionedSchemaV1 !== null) { + return { type: 'unversioned', version: 1, buffer: unversionedSchemaV1 }; + } + return null; } /** diff --git a/packages/common/src/types/chainUpdate.ts b/packages/common/src/types/chainUpdate.ts index 44314178a..97d1f6832 100644 --- a/packages/common/src/types/chainUpdate.ts +++ b/packages/common/src/types/chainUpdate.ts @@ -4,7 +4,6 @@ import { AuthorizationsV1, Base58String, Duration, - Energy, FinalizationCommitteeParameters, GasRewardsV0, GasRewardsV1, @@ -12,6 +11,7 @@ import { TimeoutParameters, } from '..'; import type { + Energy, IpInfo, ArInfo, VerifyKey, diff --git a/packages/common/src/types/moduleReference.ts b/packages/common/src/types/moduleReference.ts index 35137bd15..35088ff5b 100644 --- a/packages/common/src/types/moduleReference.ts +++ b/packages/common/src/types/moduleReference.ts @@ -27,8 +27,8 @@ export class ModuleReference { } } - static fromBytes(bytes: Buffer): ModuleReference { - return new ModuleReference(bytes.toString('hex')); + static fromBytes(bytes: ArrayBuffer): ModuleReference { + return new ModuleReference(Buffer.from(bytes).toString('hex')); } toJSON(): string { diff --git a/packages/common/src/util.ts b/packages/common/src/util.ts index 081369954..570ec50d4 100644 --- a/packages/common/src/util.ts +++ b/packages/common/src/util.ts @@ -150,7 +150,10 @@ export function countSignatures( */ export function wasmToSchema(wasm: Buffer): Buffer { const wasmModule = new WebAssembly.Module(wasm); - const schemaBytes = schemaBytesFromWasmModule(wasmModule); + const schemaBytes = schemaBytesFromWasmModule( + wasmModule, + 'concordium-schema' + ); if (schemaBytes === null) { throw Error('WASM-Module contains no schema!'); } @@ -163,12 +166,13 @@ export function wasmToSchema(wasm: Buffer): Buffer { * @returns the smart contract schema as a Buffer or null if not present. */ export function schemaBytesFromWasmModule( - wasmModule: WebAssembly.Module + wasmModule: WebAssembly.Module, + sectionName: + | 'concordium-schema' + | 'concordium-schema-v1' + | 'concordium-schema-v2' ): Buffer | null { - const sections = WebAssembly.Module.customSections( - wasmModule, - 'concordium-schema' - ); + const sections = WebAssembly.Module.customSections(wasmModule, sectionName); if (sections.length === 1) { return Buffer.from(sections[0]); } else if (sections.length === 0) {